source: trunk/email2trac.py.in @ 484

Last change on this file since 484 was 484, checked in by bas, 14 years ago

Applied a patch to detect valid users if smtp_default_domain is set, see #219

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 59.0 KB
RevLine 
[22]1#!@PYTHON@
2# Copyright (C) 2002
3#
4# This file is part of the email2trac utils
5#
6# This program is free software; you can redistribute it and/or modify it
7# under the terms of the GNU General Public License as published by the
8# Free Software Foundation; either version 2, or (at your option) any
9# later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
19#
[80]20# For vi/emacs or other use tabstop=4 (vi: set ts=4)
21#
[22]22"""
[54]23email2trac.py -- Email tickets to Trac.
[22]24
25A simple MTA filter to create Trac tickets from inbound emails.
26
27Copyright 2005, Daniel Lundin <daniel@edgewall.com>
28Copyright 2005, Edgewall Software
29
[282]30Authors:
31  Bas van der Vlies <basv@sara.nl>
32  Walter de Jong <walter@sara.nl>
[22]33
34The scripts reads emails from stdin and inserts directly into a Trac database.
35
36How to use
37----------
[282]38 * See https://subtrac.sara.nl/oss/email2trac/
39
[22]40 * Create an config file:
[282]41    [DEFAULT]                        # REQUIRED
42    project      : /data/trac/test   # REQUIRED
43    debug        : 1                 # OPTIONAL, if set print some DEBUG info
[22]44
[282]45    [jouvin]                         # OPTIONAL project declaration, if set both fields necessary
46    project      : /data/trac/jouvin # use -p|--project jouvin. 
[22]47       
48 * default config file is : /etc/email2trac.conf
49
50 * Commandline opions:
[205]51                -h,--help
[415]52                                -d, --debug
[205]53                -f,--file  <configuration file>
[410]54                -n,--dry-run
[205]55                -p, --project <project name>
[415]56                -t, --ticket_prefix <name>
[22]57
58SVN Info:
59        $Id: email2trac.py.in 484 2010-10-06 12:07:19Z bas $
60"""
61import os
62import sys
63import string
64import getopt
65import time
66import email
[136]67import email.Iterators
68import email.Header
[22]69import re
70import urllib
71import unicodedata
72import mimetypes
[96]73import traceback
[403]74import logging
75import logging.handlers
[404]76import UserDict
[433]77
[411]78from datetime import tzinfo, timedelta, datetime
[433]79from stat import *
[22]80
[403]81
[363]82from trac import __version__ as trac_version
[199]83from trac import config as trac_config
[91]84
[96]85# Some global variables
86#
87m = None
[22]88
[404]89class SaraDict(UserDict.UserDict):
90        def __init__(self, dictin = None):
91                UserDict.UserDict.__init__(self)
92                self.name = None
93               
94                if dictin:
95                        if dictin.has_key('name'):
96                                self.name = dictin['name']
97                                del dictin['name']
98                        self.data = dictin
99                       
[452]100        def get_value(self, name):
101                if self.has_key(name):
102                        return self[name]
[404]103                else:
104                        return None
105                               
106        def __repr__(self):
107                return repr(self.data)
[182]108
[404]109        def __str__(self):
110                return str(self.data)
111                       
112        def __getattr__(self, name):
113                """
114                override the class attribute get method. Return the value
[409]115                from the dictionary
[404]116                """
117                if self.data.has_key(name):
118                        return self.data[name]
119                else:
120                        return None
121                       
122        def __setattr__(self, name, value):
123                """
124                override the class attribute set method only when the UserDict
125                has set its class attribute
126                """
127                if self.__dict__.has_key('data'):
128                        self.data[name] = value
129                else:
130                        self.__dict__[name] = value
131
132        def __iter__(self):
133                return iter(self.data.keys())
134
[22]135class TicketEmailParser(object):
136        env = None
137        comment = '> '
[359]138
[410]139        def __init__(self, env, parameters, logger, version):
[22]140                self.env = env
141
142                # Database connection
143                #
144                self.db = None
145
[206]146                # Save parameters
147                #
148                self.parameters = parameters
[410]149                self.logger = logger
[206]150
[72]151                # Some useful mail constants
152                #
[287]153                self.email_name = None
[72]154                self.email_addr = None
[183]155                self.email_from = None
[288]156                self.author     = None
[287]157                self.id         = None
[294]158               
159                self.STRIP_CONTENT_TYPES = list()
[72]160
[452]161                ## fields properties via body_text
162                #
163                self.properties = dict()
164
[22]165                self.VERSION = version
[402]166
[404]167                self.get_config = self.env.config.get
168
[410]169                ## init function ##
170                #
[402]171                self.setup_parameters()
172
[404]173        def setup_parameters(self):
[434]174                if self.parameters.umask:
[452]175                        os.umask(self.parameters.umask)
[402]176
[452]177                if not self.parameters.spam_level:
[435]178                        self.parameters.spam_level = 0
[22]179
[421]180                if not self.parameters.spam_header:
181                        self.parameters.spam_header = 'X-Spam-Score'
[207]182
[437]183                if not self.parameters.email_quote:
[435]184                        self.parameters.email_quote = '> '
[22]185
[452]186                if not self.parameters.ticket_update_by_subject_lookback:
[445]187                        self.parameters.ticket_update_by_subject_lookback = 30
[360]188
[448]189                if self.parameters.verbatim_format == None:
[446]190                        self.parameters.verbatim_format = 1
[118]191
[448]192                if self.parameters.reflow == None:
[447]193                        self.parameters.reflow = 1
[231]194
[450]195                if self.parameters.binhex:
[294]196                        self.STRIP_CONTENT_TYPES.append('application/mac-binhex40')
[238]197
[450]198                if self.parameters.applesingle:
[294]199                        self.STRIP_CONTENT_TYPES.append('application/applefile')
[238]200
[450]201                if self.parameters.appledouble:
[294]202                        self.STRIP_CONTENT_TYPES.append('application/applefile')
[238]203
[450]204                if self.parameters.strip_content_types:
205                        items = self.parameters.strip_content_types.split(',')
[294]206                        for item in items:
207                                self.STRIP_CONTENT_TYPES.append(item.strip())
208
[450]209                if self.parameters.tmpdir:
210                        self.parameters.tmpdir = os.path.normcase(str(self.parameters['tmpdir']))
211                else:
212                        self.parameters.tmpdir = os.path.normcase('/tmp')
[257]213
[450]214                if self.parameters.email_triggers_workflow == None:
215                        self.parameters.email_triggers_workflow = 1
[191]216
[451]217                if not self.parameters.subject_field_separator:
218                        self.parameters.subject_field_separator = '&'
[297]219                else:
[451]220                        self.parameters.subject_field_separator = self.parameters.subject_field_separator.strip()
[297]221
[305]222                self.trac_smtp_from = self.get_config('notification', 'smtp_from')
[484]223                self.smtp_default_domain = self.get_config('notification', 'smtp_default_domain')
[305]224
[484]225
[359]226                self.system = None
227
[341]228########## Email Header Functions ###########################################################
[339]229
[22]230        def spam(self, message):
[191]231                """
232                # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
233                # Note if Spam_level then '*' are included
234                """
[194]235                spam = False
[421]236                if message.has_key(self.parameters.spam_header):
237                        spam_l = string.split(message[self.parameters.spam_header])
[22]238
[207]239                        try:
240                                number = spam_l[0].count('*')
241                        except IndexError, detail:
242                                number = 0
243                               
[435]244                        if number >= self.parameters.spam_level:
[194]245                                spam = True
246                               
[191]247                # treat virus mails as spam
248                #
249                elif message.has_key('X-Virus-found'):                 
[194]250                        spam = True
251
252                # How to handle SPAM messages
253                #
[440]254                if self.parameters.drop_spam and spam:
[194]255
[435]256                        self.logger.info('Message is a SPAM. Automatic ticket insertion refused (SPAM level > %d)' %self.parameters.spam_level)
[204]257                        return 'drop'   
[194]258
259                elif spam:
260
[204]261                        return 'Spam'   
[194]262                else:
[22]263
[204]264                        return False
[191]265
[221]266        def email_header_acl(self, keyword, header_field, default):
[206]267                """
[221]268                This function wil check if the email address is allowed or denied
269                to send mail to the ticket list
270            """
[409]271                self.logger.debug('function email_header_acl: %s' %keyword)
272
[206]273                try:
[221]274                        mail_addresses = self.parameters[keyword]
275
276                        # Check if we have an empty string
277                        #
278                        if not mail_addresses:
279                                return default
280
[206]281                except KeyError, detail:
[416]282                        self.logger.debug('%s not defined, all messages are allowed.' %(keyword))
[206]283
[221]284                        return default
[206]285
[221]286                mail_addresses = string.split(mail_addresses, ',')
287
288                for entry in mail_addresses:
[209]289                        entry = entry.strip()
[221]290                        TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
291                        result =  TO_RE.search(header_field)
[208]292                        if result:
293                                return True
[149]294
[208]295                return False
296
[22]297        def email_header_txt(self, m):
[72]298                """
299                Display To and CC addresses in description field
300                """
[288]301                s = ''
[334]302
[213]303                if m['To'] and len(m['To']) > 0:
[288]304                        s = "'''To:''' %s\r\n" %(m['To'])
[22]305                if m['Cc'] and len(m['Cc']) > 0:
[288]306                        s = "%s'''Cc:''' %s\r\n" % (s, m['Cc'])
[22]307
[288]308                return  self.email_to_unicode(s)
[22]309
[138]310
[194]311        def get_sender_info(self, message):
[45]312                """
[72]313                Get the default author name and email address from the message
[226]314                """
[43]315
[226]316                self.email_to = self.email_to_unicode(message['to'])
317                self.to_name, self.to_email_addr = email.Utils.parseaddr (self.email_to)
318
[194]319                self.email_from = self.email_to_unicode(message['from'])
[287]320                self.email_name, self.email_addr  = email.Utils.parseaddr(self.email_from)
[142]321
[304]322                ## Trac can not handle author's name that contains spaces
[477]323                #
[305]324                if self.email_addr == self.trac_smtp_from:
[395]325                        if self.email_name:
326                                self.author = self.email_name
327                        else:
328                                self.author = "email2trac"
[304]329                else:
330                        self.author = self.email_addr
331
[450]332                if self.parameters.ignore_trac_user_settings:
[194]333                        return
334
335                # Is this a registered user, use email address as search key:
336                # result:
337                #   u : login name
338                #   n : Name that the user has set in the settings tab
339                #   e : email address that the user has set in the settings tab
[45]340                #
[194]341                users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
[484]342                        if (
343                                (e and (e.lower() == self.email_addr.lower())) or
344                            (u + '@' + self.smtp_default_domain.lower() == self.email_addr.lower())
345                        )
346                        ]
[43]347
[483]348                if len(users) >= 1:
[194]349                        self.email_from = users[0][0]
[250]350                        self.author = users[0][0]
[45]351
[72]352        def set_reply_fields(self, ticket, message):
353                """
354                Set all the right fields for a new ticket
355                """
[409]356                self.logger.debug('function set_reply_fields')
[72]357
[270]358                ## Only use name or email adress
359                #ticket['reporter'] = self.email_from
360                ticket['reporter'] = self.author
361
362
[45]363                # Put all CC-addresses in ticket CC field
[43]364                #
[439]365                if self.parameters.reply_all:
[43]366
[299]367                        email_cc = ''
368
369                        cc_addrs = email.Utils.getaddresses( message.get_all('cc', []) )
370
371                        if not cc_addrs:
[105]372                                return
[43]373
[299]374                        ## Build a list of forbidden CC addresses
375                        #
[300]376                        #to_addrs = email.Utils.getaddresses( message.get_all('to', []) )
377                        #to_list = list()
378                        #for n,e in to_addrs:
379                        #       to_list.append(e)
[299]380                               
[392]381                        # Always Remove reporter email address from cc-list
[43]382                        #
[392]383                        try:
384                                cc_addrs.remove((self.author, self.email_addr))
385                        except ValueError, detail:
386                                pass
[43]387
[299]388                        for name,addr in cc_addrs:
389               
390                                ## Prevent mail loop
391                                #
[300]392                                #if addr in to_list:
[304]393
394                                if addr == self.trac_smtp_from:
[416]395                                        self.logger.debug("Skipping %s mail address for CC-field" %(addr))
[299]396                                        continue
[43]397
[299]398                                if email_cc:
399                                        email_cc = '%s, %s' %(email_cc, addr)
400                                else:
401                                        email_cc = addr
[96]402
[299]403                        if email_cc:
[416]404                                self.logger.debug('set_reply_fields: %s' %email_cc)
[299]405
406                                ticket['cc'] = self.email_to_unicode(email_cc)
407
[339]408
409########## DEBUG functions  ###########################################################
410
[310]411        def debug_body(self, message_body, tempfile=False):
412                if tempfile:
413                        import tempfile
414                        body_file = tempfile.mktemp('.email2trac')
415                else:
[450]416                        body_file = os.path.join(self.parameters.tmpdir, 'body.txt')
[310]417
[407]418                if self.parameters.dry_run:
[331]419                        print 'DRY-RUN: not saving body to %s' %(body_file)
420                        return
421
[433]422                print 'writing body to %s' %(body_file)
[331]423                fx = open(body_file, 'wb')
[310]424                if not message_body:
[331]425                                message_body = '(None)'
[310]426
427                message_body = message_body.encode('utf-8')
428                #message_body = unicode(message_body, 'iso-8859-15')
429
430                fx.write(message_body)
431                fx.close()
432                try:
433                        os.chmod(body_file,S_IRWXU|S_IRWXG|S_IRWXO)
434                except OSError:
435                        pass
436
437        def debug_attachments(self, message_parts):
[317]438                """
439                """
[409]440                self.logger.debug('function debug_attachments')
[317]441               
[310]442                n = 0
[331]443                for item in message_parts:
[310]444                        # Skip inline text parts
[331]445                        if not isinstance(item, tuple):
[310]446                                continue
447                               
[331]448                        (original, filename, part) = item
[310]449
450                        n = n + 1
[433]451                        print 'part%d: Content-Type: %s' % (n, part.get_content_type())
[379]452               
[433]453                        s = 'part%d: filename: %s' %(n, filename)
[379]454                        self.print_unicode(s)
455       
[348]456                        ## Forbidden chars
457                        #
458                        filename = filename.replace('\\', '_')
459                        filename = filename.replace('/', '_')
460       
461
[450]462                        part_file = os.path.join(self.parameters.tmpdir, filename)
[433]463                        s = 'writing part%d (%s)' % (n,part_file)
[379]464                        self.print_unicode(s)
[331]465
[407]466                        if self.parameters.dry_run:
[331]467                                print 'DRY_RUN: NOT saving attachments'
468                                continue
469
[377]470                        part_file = util.text.unicode_quote(part_file)
471
[310]472                        fx = open(part_file, 'wb')
473                        text = part.get_payload(decode=1)
[331]474
[310]475                        if not text:
476                                text = '(None)'
[331]477
[310]478                        fx.write(text)
479                        fx.close()
[331]480
[310]481                        try:
482                                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
483                        except OSError:
484                                pass
485
[466]486        def save_email_for_debug(self, message, create_tempfile=False):
[309]487
[466]488                if create_tempfile:
[96]489                        import tempfile
490                        msg_file = tempfile.mktemp('.email2trac')
491                else:
[173]492                        #msg_file = '/var/tmp/msg.txt'
[450]493                        msg_file = os.path.join(self.parameters.tmpdir, 'msg.txt')
[173]494
[407]495                if self.parameters.dry_run:
[331]496                        print 'DRY_RUN: NOT saving email message to %s' %(msg_file)
497                else:
[433]498                        print 'saving email to %s' %(msg_file)
[44]499
[331]500                        fx = open(msg_file, 'wb')
501                        fx.write('%s' % message)
502                        fx.close()
503                       
504                        try:
505                                os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
506                        except OSError:
507                                pass
508
[309]509                message_parts = self.get_message_parts(message)
510                message_parts = self.unique_attachment_names(message_parts)
511                body_text = self.body_text(message_parts)
512                self.debug_body(body_text, True)
513                self.debug_attachments(message_parts)
514
[339]515########## Conversion functions  ###########################################################
516
[341]517        def email_to_unicode(self, message_str):
518                """
519                Email has 7 bit ASCII code, convert it to unicode with the charset
520                that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
521                understands it.
522                """
[409]523                self.logger.debug("function email_to_unicode")
[341]524
525                results =  email.Header.decode_header(message_str)
526
527                s = None
528                for text,format in results:
529                        if format:
530                                try:
531                                        temp = unicode(text, format)
532                                except UnicodeError, detail:
533                                        # This always works
534                                        #
535                                        temp = unicode(text, 'iso-8859-15')
536                                except LookupError, detail:
537                                        #text = 'ERROR: Could not find charset: %s, please install' %format
538                                        #temp = unicode(text, 'iso-8859-15')
539                                        temp = message_str
540                                       
541                        else:
542                                temp = string.strip(text)
543                                temp = unicode(text, 'iso-8859-15')
544
545                        if s:
546                                s = '%s %s' %(s, temp)
547                        else:
548                                s = '%s' %temp
549
550                #s = s.encode('utf-8')
551                return s
552
[288]553        def str_to_dict(self, s):
[164]554                """
[288]555                Transfrom a string of the form [<key>=<value>]+ to dict[<key>] = <value>
[164]556                """
[409]557                self.logger.debug("function str_to_dict")
[164]558
[451]559                fields = string.split(s, self.parameters.subject_field_separator)
[262]560
[164]561                result = dict()
562                for field in fields:
563                        try:
[262]564                                index, value = string.split(field, '=')
[169]565
566                                # We can not change the description of a ticket via the subject
567                                # line. The description is the body of the email
568                                #
569                                if index.lower() in ['description']:
570                                        continue
571
[164]572                                if value:
[165]573                                        result[index.lower()] = value
[169]574
[164]575                        except ValueError:
576                                pass
[165]577                return result
[167]578
[379]579        def print_unicode(self,s):
580                """
581                This function prints unicode strings uif possible else it will quote it
582                """
583                try:
[408]584                        self.logger.debug(s)
[379]585                except UnicodeEncodeError, detail:
[408]586                        self.logger.debug(util.text.unicode_quote(s))
[379]587
[339]588########## TRAC ticket functions  ###########################################################
589
[474]590        def check_permission_participants(self, tkt, action):
[397]591                """
[398]592                Check if the mailer is allowed to update the ticket
593                """
[409]594                self.logger.debug('function check_permission_participants')
[398]595
596                if tkt['reporter'].lower() in [self.author, self.email_addr]:
[416]597                        self.logger.debug('ALLOW, %s is the ticket reporter' %(self.email_addr))
[408]598
[398]599                        return True
600
601                perm = PermissionSystem(self.env)
[474]602                if perm.check_permission(action, self.author):
[416]603                        self.logger.debug('ALLOW, %s has trac permission to update the ticket' %(self.author))
[408]604
[398]605                        return True
606               
607                # Is the updater in the CC?
608                try:
609                        cc_list = tkt['cc'].split(',')
610                        for cc in cc_list:
611                                if self.email_addr.lower() in cc.strip():
[416]612                                        self.logger.debug('ALLOW, %s is in the CC' %(self.email_addr))
[398]613
614                                        return True
615
616                except KeyError:
[474]617                        pass
[398]618
[474]619                return False
620
[398]621        def check_permission(self, tkt, action):
622                """
[397]623                check if the reporter has the right permission for the action:
624          - TICKET_CREATE
625          - TICKET_MODIFY
[474]626          - TICKET_APPEND
627          - TICKET_CHGPROP
[397]628
629                There are three models:
630                        - None      : no checking at all
631                        - trac      : check the permission via trac permission model
632                        - email2trac: ....
633                """
[409]634                self.logger.debug("function check_permission")
[397]635
[441]636                if self.parameters.ticket_permission_system in ['trac']:
[398]637
[397]638                        perm = PermissionSystem(self.env)
639                        if perm.check_permission(action, self.author):
640                                return True
641                        else:
642                                return False
[398]643
[441]644                elif self.parameters.ticket_permission_system in ['update_restricted_to_participants']:
[474]645                        return (self.check_permission_participants(tkt, action))       
[398]646
[477]647                ## Default is to allow everybody ticket updates and ticket creation
648                #
[397]649                else:
650                                return True
651
652
[476]653        def update_ticket_fields(self, ticket, user_dict, new=None):
[202]654                """
655                This will update the ticket fields. It will check if the
656                given fields are known and if the right values are specified
657                It will only update the ticket field value:
[169]658                        - If the field is known
[202]659                        - If the value supplied is valid for the ticket field.
660                          If not then there are two options:
[474]661                           1) Skip the value (new_ticket=None)
662                           2) Set default value for field (new_ticket=1)
[169]663                """
[409]664                self.logger.debug("function update_ticket_fields")
[169]665
[474]666                ## Check only permission model on ticket updates
667                #
[476]668                if not new:
[474]669                        if self.parameters.ticket_permission_system:
670                                if not self.check_permission(ticket, 'TICKET_CHGPROP'):
671                                        self.logger.info('Reporter: %s has no permission to change ticket properties' %self.author)
672                                        return False
673
[477]674                ## Build a system dictionary from the ticket fields
675                #  with field as index and option as value
[169]676                #
677                sys_dict = dict()
678                for field in ticket.fields:
[167]679                        try:
[169]680                                sys_dict[field['name']] = field['options']
681
[167]682                        except KeyError:
[169]683                                sys_dict[field['name']] = None
[167]684                                pass
[169]685
[301]686                ## Check user supplied fields an compare them with the
[477]687                #  system one's
[169]688                #
689                for field,value in user_dict.items():
[408]690                        if self.parameters.debug:
[433]691                                s = 'user_field\t %s = %s' %(field,value)
[379]692                                self.print_unicode(s)
[169]693
[301]694                        ## To prevent mail loop
695                        #
696                        if field == 'cc':
697
698                                cc_list = user_dict['cc'].split(',')
699
[304]700                                if self.trac_smtp_from in cc_list:
[433]701                                        self.logger.debug('MAIL LOOP: %s is not allowed as CC address' %(self.trac_smtp_from))
[408]702
[304]703                                        cc_list.remove(self.trac_smtp_from)
[301]704
705                                value = ','.join(cc_list)
706                               
707
[169]708                        if sys_dict.has_key(field):
709
[477]710                                ## Check if value is an allowed system option, if TypeError then
711                                #  every value is allowed
[169]712                                #
713                                try:
714                                        if value in sys_dict[field]:
715                                                ticket[field] = value
[202]716                                        else:
[477]717                                                ## Must we set a default if value is not allowed
[202]718                                                #
[476]719                                                if new:
[202]720                                                        value = self.get_config('ticket', 'default_%s' %(field) )
[169]721
722                                except TypeError:
[345]723                                        pass
724
725                                ## Only set if we have a value
726                                #
727                                if value:
[169]728                                        ticket[field] = value
[202]729
[408]730                                if self.parameters.debug:
[379]731                                        s = 'ticket_field\t %s = %s' %(field,  ticket[field])
732                                        self.print_unicode(s)
[345]733
[260]734        def ticket_update(self, m, id, spam):
[78]735                """
[79]736                If the current email is a reply to an existing ticket, this function
737                will append the contents of this email to that ticket, instead of
738                creating a new one.
[78]739                """
[409]740                self.logger.debug("function ticket_update")
[202]741
[456]742                if not self.parameters.ticket_update:
743                        self.logger.debug("ticket_update disabled")
744                        return False
745
[477]746                ## Must we update ticket fields
[164]747                #
[220]748                update_fields = dict()
[165]749                try:
[260]750                        id, keywords = string.split(id, '?')
[262]751
[220]752                        update_fields = self.str_to_dict(keywords)
[165]753
[477]754                        ## Strip '#'
[165]755                        #
[260]756                        self.id = int(id[1:])
[165]757
[260]758                except ValueError:
[390]759
[477]760                        ## Strip '#'
[165]761                        #
[362]762                        self.id = int(id[1:])
[164]763
[456]764                self.logger.debug("ticket_update id %s" %id)
[71]765
[477]766                ## When is the change committed
[194]767                #
[386]768                if self.VERSION < 0.11:
769                        when = int(time.time())
770                else:
[431]771                        when = datetime.now(util.datefmt.utc)
[77]772
[172]773                try:
[253]774                        tkt = Ticket(self.env, self.id, self.db)
[390]775
[172]776                except util.TracError, detail:
[390]777
[477]778                        ## Not a valid ticket
779                        #
[253]780                        self.id = None
[172]781                        return False
[126]782
[477]783                ## Check the permission of the reporter
[398]784                #
[441]785                if self.parameters.ticket_permission_system:
[474]786                        if not self.check_permission(tkt, 'TICKET_APPEND'):
787                                self.logger.info('Reporter: %s has no permission to add comments or attachments to tickets' %self.author)
[398]788                                return False
789
[468]790                ## How many changes has this ticket
791                #
[288]792                cnum = len(tkt.get_changelog())
793
[468]794                ## reopen the ticket if it is was closed
795                #  We must use the ticket workflow framework
[220]796                #
[469]797                if self.parameters.email_triggers_workflow and (self.VERSION >= 0.11):
[220]798
[469]799                        self.logger.debug('Workflow ticket update fields: ')
[257]800
[469]801                        from trac.ticket.default_workflow import ConfigurableTicketWorkflow
802                        from trac.test import Mock, MockPerm
[468]803
[469]804                        req = Mock(authname=self.author, perm=MockPerm(), args={})
805                        try:
[468]806                                workflow = self.parameters['workflow_%s' %tkt['status']]
[469]807                        except KeyError:
[468]808                                ## fallback for compability (Will be deprecated)
809                                #
[469]810                                if tkt['status'] in ['closed']:
811                                        workflow = self.parameters.workflow
812                                else:   
813                                        workflow = None
[468]814
[469]815                        controller = ConfigurableTicketWorkflow(self.env)
816                        #print controller.actions
817                        #print controller.actions.keys()
818                        #print controller.get_ticket_actions(req, tkt)
819                        #print controller.actions[workflow]
820                        #print controller.actions[workflow]['permissions'] is a list
821
[468]822                        if workflow:
[257]823
[469]824                                if self.parameters.ticket_permission_system:
[257]825
[469]826                                        if self.check_permission(tkt, controller.actions[workflow]['permissions'][0]):
827                                                fields = controller.get_ticket_changes(req, tkt, workflow)
828                                        else:
829                                                fields = dict()
830                                                self.logger.info('Reporter: %s has no permission to trigger workflow' %self.author)
[257]831
[469]832                                else:
833                                        fields = controller.get_ticket_changes(req, tkt, workflow)
[257]834
835                                for key in fields.keys():
[416]836                                        self.logger.debug('\t %s : %s' %(key, fields[key]))
[257]837                                        tkt[key] = fields[key]
838
[469]839                ## Old pre 0.11 situation
840                #
841                elif self.parameters.email_triggers_workflow:
[470]842
[469]843                        self.logger.debug('email triggers workflow pre trac 0.11')
[470]844
[469]845                        if tkt['status'] in ['closed']:
[257]846                                tkt['status'] = 'reopened'
847                                tkt['resolution'] = ''
848
[469]849                else:
850                        self.logger.debug('email triggers workflow disabled')
851
852                ## Must we update some ticket fields properties via subjectline
[172]853                #
[220]854                if update_fields:
855                        self.update_ticket_fields(tkt, update_fields)
[166]856
[236]857                message_parts = self.get_message_parts(m)
[253]858                message_parts = self.unique_attachment_names(message_parts)
[210]859
[477]860                ## Must we update some ticket fields properties via body_text
[309]861                #
862                if self.properties:
863                                self.update_ticket_fields(tkt, self.properties)
864
[436]865                if self.parameters.email_header:
[236]866                        message_parts.insert(0, self.email_header_txt(m))
[76]867
[236]868                body_text = self.body_text(message_parts)
869
[401]870                error_with_attachments = self.attach_attachments(message_parts)
[348]871
[309]872                if body_text.strip() or update_fields or self.properties:
[407]873                        if self.parameters.dry_run:
[288]874                                print 'DRY_RUN: tkt.save_changes(self.author, body_text, ticket_change_number) ', self.author, cnum
[250]875                        else:
[348]876                                if error_with_attachments:
877                                        body_text = '%s\\%s' %(error_with_attachments, body_text)
[431]878                                self.logger.debug('tkt.save_changes(%s, %d)' %(self.author, cnum))
[288]879                                tkt.save_changes(self.author, body_text, when, None, str(cnum))
880                       
[219]881
[392]882                if not spam:
[253]883                        self.notify(tkt, False, when)
[72]884
[71]885                return True
886
[202]887        def set_ticket_fields(self, ticket):
[77]888                """
[202]889                set the ticket fields to value specified
890                        - /etc/email2trac.conf with <prefix>_<field>
891                        - trac default values, trac.ini
892                """
[410]893                self.logger.debug('function set_ticket_fields')
894
[202]895                user_dict = dict()
896
897                for field in ticket.fields:
898
899                        name = field['name']
900
[335]901                        ## default trac value
[202]902                        #
[233]903                        if not field.get('custom'):
904                                value = self.get_config('ticket', 'default_%s' %(name) )
[472]905                                if (name in ['resolution']) and (value in ['fixed']):
906                                        value = None
[233]907                        else:
[345]908                                ##  Else we get the default value for reporter
909                                #
[233]910                                value = field.get('value')
911                                options = field.get('options')
[345]912
[335]913                                if value and options and (value not in options):
[345]914                                         value = options[int(value)]
915       
[408]916                        if self.parameters.debug:
[416]917                                s = 'trac[%s] = %s' %(name, value)
[379]918                                self.print_unicode(s)
[202]919
[335]920                        ## email2trac.conf settings
921                        #
[472]922                        prefix = self.parameters.ticket_prefix
[202]923                        try:
[206]924                                value = self.parameters['%s_%s' %(prefix, name)]
[467]925                                if self.parameters.debug:
[416]926                                        s = 'email2trac[%s] = %s ' %(name, value)
[379]927                                        self.print_unicode(s)
[202]928
929                        except KeyError, detail:
930                                pass
931               
[345]932                        if value:
933                                user_dict[name] = value
[472]934                                if self.parameters.debug:
935                                        s = 'used %s = %s' %(name, value)
936                                        self.print_unicode(s)
[202]937
[476]938                self.update_ticket_fields(ticket, user_dict, new=1)
[202]939
[352]940                if 'status' not in user_dict.keys():
941                        ticket['status'] = 'new'
[202]942
943
[356]944        def ticket_update_by_subject(self, subject):
945                """
946                This list of Re: prefixes is probably incomplete. Taken from
947                wikipedia. Here is how the subject is matched
948                  - Re: <subject>
949                  - Re: (<Mail list label>:)+ <subject>
[202]950
[356]951                So we must have the last column
952                """
[409]953                self.logger.debug('function ticket_update_by_subject')
[356]954
955                matched_id = None
[444]956                if self.parameters.ticket_update and self.parameters.ticket_update_by_subject:
[356]957                               
958                        SUBJECT_RE = re.compile(r'^(RE|AW|VS|SV):(.*:)*\s*(.*)', re.IGNORECASE)
959                        result = SUBJECT_RE.search(subject)
960
961                        if result:
[477]962                                ## This is a reply
963                                #
[356]964                                orig_subject = result.group(3)
965
[416]966                                self.logger.debug('subject search string: %s' %(orig_subject))
[356]967
968                                cursor = self.db.cursor()
969                                summaries = [orig_subject, '%%: %s' % orig_subject]
970
[477]971                                ## Convert days to seconds
972                                #
[360]973                                lookback = int(time.mktime(time.gmtime())) - \
[445]974                                                self.parameters.ticket_update_by_subject_lookback * 24 * 3600
[356]975
[360]976
[356]977                                for summary in summaries:
[416]978                                        self.logger.debug('Looking for summary matching: "%s"' % summary)
[408]979
[356]980                                        sql = """SELECT id FROM ticket
981                                                        WHERE changetime >= %s AND summary LIKE %s
982                                                        ORDER BY changetime DESC"""
983                                        cursor.execute(sql, [lookback, summary.strip()])
984
985                                        for row in cursor:
986                                                (matched_id,) = row
[408]987
[416]988                                                self.logger.debug('Found matching ticket id: %d' % matched_id)
[408]989
[356]990                                                break
991
992                                        if matched_id:
[366]993                                                matched_id = '#%d' % matched_id
[356]994                                                return matched_id
995
996                return matched_id
997
998
[262]999        def new_ticket(self, msg, subject, spam, set_fields = None):
[202]1000                """
[77]1001                Create a new ticket
1002                """
[409]1003                self.logger.debug('function new_ticket')
[250]1004
[41]1005                tkt = Ticket(self.env)
[326]1006
1007                self.set_reply_fields(tkt, msg)
1008
[202]1009                self.set_ticket_fields(tkt)
1010
[477]1011                ## Check the permission of the reporter
[397]1012                #
[441]1013                if self.parameters.ticket_permission_system:
[398]1014                        if not self.check_permission(tkt, 'TICKET_CREATE'):
[409]1015                                self.logger.info('Reporter: %s has no permission to create tickets' %self.author)
[397]1016                                return False
1017
[477]1018                ## Old style setting for component, will be removed
[202]1019                #
[204]1020                if spam:
1021                        tkt['component'] = 'Spam'
1022
[206]1023                elif self.parameters.has_key('component'):
1024                        tkt['component'] = self.parameters['component']
[201]1025
[22]1026                if not msg['Subject']:
[151]1027                        tkt['summary'] = u'(No subject)'
[22]1028                else:
[264]1029                        tkt['summary'] = subject
[22]1030
1031
[262]1032                if set_fields:
1033                        rest, keywords = string.split(set_fields, '?')
1034
1035                        if keywords:
1036                                update_fields = self.str_to_dict(keywords)
1037                                self.update_ticket_fields(tkt, update_fields)
1038
[296]1039
[236]1040                message_parts = self.get_message_parts(msg)
[309]1041
[477]1042                ## Must we update some ticket fields properties via body_text
[309]1043                #
1044                if self.properties:
1045                                self.update_ticket_fields(tkt, self.properties)
1046
[236]1047                message_parts = self.unique_attachment_names(message_parts)
1048               
[477]1049                ## produce e-mail like header
[436]1050                #
1051                head = ''
1052                if self.parameters.email_header:
1053                        head = self.email_header_txt(msg)
1054                        message_parts.insert(0, head)
[236]1055                       
1056                body_text = self.body_text(message_parts)
[45]1057
[236]1058                tkt['description'] = body_text
[90]1059
[477]1060                ## When is the change committed
[431]1061                #
1062                if self.VERSION < 0.11:
1063                        when = int(time.time())
1064                else:
1065                        when = datetime.now(util.datefmt.utc)
[45]1066
[408]1067                if self.parameters.dry_run:
1068                        print 'DRY_RUN: tkt.insert()'
1069                else:
[253]1070                        self.id = tkt.insert()
[273]1071       
[90]1072                changed = False
1073                comment = ''
[77]1074
[477]1075                ## some routines in trac are dependend on ticket id     
1076                #  like alternate notify template
[273]1077                #
[438]1078                if self.parameters.alternate_notify_template:
[274]1079                        tkt['id'] = self.id
[273]1080                        changed = True
1081
[295]1082                ## Rewrite the description if we have mailto enabled
[45]1083                #
[434]1084                if self.parameters.mailto_link:
[100]1085                        changed = True
[142]1086                        comment = u'\nadded mailto line\n'
[343]1087                        mailto = self.html_mailto_link( m['Subject'])
[253]1088
[213]1089                        tkt['description'] = u'%s\r\n%s%s\r\n' \
[142]1090                                %(head, mailto, body_text)
[295]1091       
1092                ## Save the attachments to the ticket   
1093                #
[340]1094                error_with_attachments =  self.attach_attachments(message_parts)
[295]1095
[319]1096                if error_with_attachments:
1097                        changed = True
1098                        comment = '%s\n%s\n' %(comment, error_with_attachments)
[45]1099
[90]1100                if changed:
[408]1101                        if self.parameters.dry_run:
[344]1102                                print 'DRY_RUN: tkt.save_changes(%s, comment) real reporter = %s' %( tkt['reporter'], self.author)
[201]1103                        else:
[344]1104                                tkt.save_changes(tkt['reporter'], comment)
[201]1105                                #print tkt.get_changelog(self.db, when)
[90]1106
[392]1107                if not spam:
[253]1108                        self.notify(tkt, True)
[45]1109
[260]1110
[342]1111        def attach_attachments(self, message_parts, update=False):
1112                '''
1113                save any attachments as files in the ticket's directory
1114                '''
[409]1115                self.logger.debug('function attach_attachments()')
[342]1116
[407]1117                if self.parameters.dry_run:
[342]1118                        print "DRY_RUN: no attachments attached to tickets"
1119                        return ''
1120
1121                count = 0
1122
[477]1123                ## Get Maxium attachment size
[342]1124                #
1125                max_size = int(self.get_config('attachment', 'max_size'))
1126                status   = None
1127               
1128                for item in message_parts:
[477]1129                        ## Skip body parts
1130                        #
[342]1131                        if not isinstance(item, tuple):
1132                                continue
1133                               
1134                        (original, filename, part) = item
[477]1135
1136                        ## We have to determine the size so we use this temporary solution. we must escape it
1137                        #  else we get UnicodeErrors.
[342]1138                        #
[450]1139                        path, fd =  util.create_unique_file(os.path.join(self.parameters.tmpdir, util.text.unicode_quote(filename)))
[342]1140                        text = part.get_payload(decode=1)
1141                        if not text:
1142                                text = '(None)'
1143                        fd.write(text)
1144                        fd.close()
1145
[477]1146                        ## get the file_size
[342]1147                        #
1148                        stats = os.lstat(path)
[433]1149                        file_size = stats[ST_SIZE]
[342]1150
[477]1151                        ## Check if the attachment size is allowed
[342]1152                        #
1153                        if (max_size != -1) and (file_size > max_size):
1154                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1155                                        %(status, original, file_size, max_size)
1156
1157                                os.unlink(path)
1158                                continue
1159                        else:
1160                                count = count + 1
1161                                       
[477]1162                        ## Insert the attachment
[342]1163                        #
1164                        fd = open(path, 'rb')
[359]1165                        if self.system == 'discussion':
1166                                att = attachment.Attachment(self.env, 'discussion', 'topic/%s'
1167                                  % (self.id,))
1168                        else:
[431]1169                                self.logger.debug('Attach %s to ticket %d' %(util.text.unicode_quote(filename), self.id))
[359]1170                                att = attachment.Attachment(self.env, 'ticket', self.id)
1171 
[477]1172                        ## This will break the ticket_update system, the body_text is vaporized
1173                        #  ;-(
[342]1174                        #
1175                        if not update:
1176                                att.author = self.author
1177                                att.description = self.email_to_unicode('Added by email2trac')
1178
[348]1179                        try:
[431]1180                                self.logger.debug('Insert atachment')
[348]1181                                att.insert(filename, fd, file_size)
1182                        except OSError, detail:
[431]1183                                self.logger.info('%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail))
[348]1184                                status = '%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail)
[342]1185
[477]1186                        ## Remove the created temporary filename
[342]1187                        #
1188                        fd.close()
1189                        os.unlink(path)
1190
1191                ## return error
1192                #
1193                return status
1194
[359]1195########## Fullblog functions  #################################################
[339]1196
[260]1197        def blog(self, id):
1198                """
1199                The blog create/update function
1200                """
[477]1201                ## import the modules
[260]1202                #
1203                from tracfullblog.core import FullBlogCore
[312]1204                from tracfullblog.model import BlogPost, BlogComment
1205                from trac.test import Mock, MockPerm
[260]1206
[477]1207                ## instantiate blog core
1208                #
[260]1209                blog = FullBlogCore(self.env)
[312]1210                req = Mock(authname='anonymous', perm=MockPerm(), args={})
1211
[260]1212                if id:
1213
[477]1214                        ## update blog
[260]1215                        #
[268]1216                        comment = BlogComment(self.env, id)
[260]1217                        comment.author = self.author
[312]1218
1219                        message_parts = self.get_message_parts(m)
1220                        comment.comment = self.body_text(message_parts)
1221
[260]1222                        blog.create_comment(req, comment)
1223
1224                else:
[477]1225                        ## create blog
[260]1226                        #
1227                        import time
1228                        post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime()))
1229
1230                        #post = BlogPost(self.env, blog._get_default_postname(self.env))
1231                       
1232                        post.author = self.author
1233                        post.title = self.email_to_unicode(m['Subject'])
[312]1234
1235                        message_parts = self.get_message_parts(m)
1236                        post.body = self.body_text(message_parts)
[260]1237                       
1238                        blog.create_post(req, post, self.author, u'Created by email2trac', False)
1239
1240
[359]1241########## Discussion functions  ##############################################
[342]1242
[359]1243        def discussion_topic(self, content, subject):
[342]1244
[477]1245                ## Import modules.
1246                #
[359]1247                from tracdiscussion.api import DiscussionApi
1248                from trac.util.datefmt import to_timestamp, utc
1249
[416]1250                self.logger.debug('Creating a new topic in forum:', self.id)
[359]1251
[477]1252                ## Get dissussion API component.
1253                #
[359]1254                api = self.env[DiscussionApi]
1255                context = self._create_context(content, subject)
1256
[477]1257                ## Get forum for new topic.
1258                #
[359]1259                forum = api.get_forum(context, self.id)
1260
[416]1261                if not forum:
1262                        self.logger.error("ERROR: Replied forum doesn't exist")
[359]1263
[477]1264                ## Prepare topic.
1265                #
[359]1266                topic = {'forum' : forum['id'],
1267                                 'subject' : context.subject,
1268                                 'time': to_timestamp(datetime.now(utc)),
1269                                 'author' : self.author,
1270                                 'subscribers' : [self.email_addr],
1271                                 'body' : self.body_text(context.content_parts)}
1272
[477]1273                ## Add topic to DB and commit it.
1274                #
[359]1275                self._add_topic(api, context, topic)
1276                self.db.commit()
1277
1278        def discussion_topic_reply(self, content, subject):
1279
[477]1280                ## Import modules.
1281                #
[359]1282                from tracdiscussion.api import DiscussionApi
1283                from trac.util.datefmt import to_timestamp, utc
1284
[416]1285                self.logger.debug('Replying to discussion topic', self.id)
[359]1286
[477]1287                ## Get dissussion API component.
1288                #
[359]1289                api = self.env[DiscussionApi]
1290                context = self._create_context(content, subject)
1291
[477]1292                ## Get replied topic.
1293                #
[359]1294                topic = api.get_topic(context, self.id)
1295
[416]1296                if not topic:
1297                        self.logger.error("ERROR: Replied topic doesn't exist")
[359]1298
[477]1299                ## Prepare message.
1300                #
[359]1301                message = {'forum' : topic['forum'],
1302                                   'topic' : topic['id'],
1303                                   'replyto' : -1,
1304                                   'time' : to_timestamp(datetime.now(utc)),
1305                                   'author' : self.author,
1306                                   'body' : self.body_text(context.content_parts)}
1307
[477]1308                ## Add message to DB and commit it.
1309                #
[359]1310                self._add_message(api, context, message)
1311                self.db.commit()
1312
1313        def discussion_message_reply(self, content, subject):
1314
[477]1315                ## Import modules.
1316                #
[359]1317                from tracdiscussion.api import DiscussionApi
1318                from trac.util.datefmt import to_timestamp, utc
1319
[433]1320                self.loggger.debug('Replying to discussion message', self.id)
[359]1321
[477]1322                ## Get dissussion API component.
1323                #
[359]1324                api = self.env[DiscussionApi]
1325                context = self._create_context(content, subject)
1326
[477]1327                ## Get replied message.
1328                #
[359]1329                message = api.get_message(context, self.id)
1330
[416]1331                if not message:
1332                        self.logger.error("ERROR: Replied message doesn't exist")
[359]1333
[477]1334                ## Prepare message.
1335                #
[359]1336                message = {'forum' : message['forum'],
1337                                   'topic' : message['topic'],
1338                                   'replyto' : message['id'],
1339                                   'time' : to_timestamp(datetime.now(utc)),
1340                                   'author' : self.author,
1341                                   'body' : self.body_text(context.content_parts)}
1342
[477]1343                ## Add message to DB and commit it.
1344                #
[359]1345                self._add_message(api, context, message)
1346                self.db.commit()
1347
1348        def _create_context(self, content, subject):
1349
[477]1350                ## Import modules.
1351                #
[359]1352                from trac.mimeview import Context
1353                from trac.web.api import Request
1354                from trac.perm import PermissionCache
1355
[477]1356                ## TODO: Read server base URL from config.
1357                #  Create request object to mockup context creation.
[359]1358                #
1359                environ = {'SERVER_PORT' : 80,
1360                                   'SERVER_NAME' : 'test',
1361                                   'REQUEST_METHOD' : 'POST',
1362                                   'wsgi.url_scheme' : 'http',
1363                                   'wsgi.input' : sys.stdin}
1364                chrome =  {'links': {},
1365                                   'scripts': [],
1366                                   'ctxtnav': [],
1367                                   'warnings': [],
1368                                   'notices': []}
1369
1370                if self.env.base_url_for_redirect:
1371                        environ['trac.base_url'] = self.env.base_url
1372
1373                req = Request(environ, None)
1374                req.chrome = chrome
1375                req.tz = 'missing'
1376                req.authname = self.author
1377                req.perm = PermissionCache(self.env, self.author)
1378
[477]1379                ## Create and return context.
1380                #
[359]1381                context = Context.from_request(req)
1382                context.realm = 'discussion-email2trac'
1383                context.cursor = self.db.cursor()
1384                context.content = content
1385                context.subject = subject
1386
[477]1387                ## Read content parts from content.
1388                #
[359]1389                context.content_parts = self.get_message_parts(content)
1390                context.content_parts = self.unique_attachment_names(
1391                  context.content_parts)
1392
1393                return context
1394
1395        def _add_topic(self, api, context, topic):
1396                context.req.perm.assert_permission('DISCUSSION_APPEND')
1397
[477]1398                ## Filter topic.
1399                #
[359]1400                for discussion_filter in api.discussion_filters:
1401                        accept, topic_or_error = discussion_filter.filter_topic(
1402                          context, topic)
1403                        if accept:
1404                                topic = topic_or_error
1405                        else:
1406                                raise TracError(topic_or_error)
1407
[477]1408                ## Add a new topic.
1409                #
[359]1410                api.add_topic(context, topic)
1411
[477]1412                ## Get inserted topic with new ID.
1413                #
[359]1414                topic = api.get_topic_by_time(context, topic['time'])
1415
[477]1416                ## Attach attachments.
1417                #
[359]1418                self.id = topic['id']
[401]1419                self.attach_attachments(context.content_parts, True)
[359]1420
[477]1421                ## Notify change listeners.
1422                #
[359]1423                for listener in api.topic_change_listeners:
1424                        listener.topic_created(context, topic)
1425
1426        def _add_message(self, api, context, message):
1427                context.req.perm.assert_permission('DISCUSSION_APPEND')
1428
[477]1429                ## Filter message.
1430                #
[359]1431                for discussion_filter in api.discussion_filters:
1432                        accept, message_or_error = discussion_filter.filter_message(
1433                          context, message)
1434                        if accept:
1435                                message = message_or_error
1436                        else:
1437                                raise TracError(message_or_error)
1438
[477]1439                ## Add message.
1440                #
[359]1441                api.add_message(context, message)
1442
[477]1443                ## Get inserted message with new ID.
1444                #
[359]1445                message = api.get_message_by_time(context, message['time'])
1446
[477]1447                ## Attach attachments.
1448                #
[359]1449                self.id = message['topic']
[401]1450                self.attach_attachments(context.content_parts, True)
[359]1451
[477]1452                ## Notify change listeners.
1453                #
[359]1454                for listener in api.message_change_listeners:
1455                        listener.message_created(context, message)
1456
1457########## MAIN function  ######################################################
1458
[77]1459        def parse(self, fp):
[356]1460                """
1461                """
[409]1462                self.logger.debug('Main function parse')
[96]1463                global m
1464
[77]1465                m = email.message_from_file(fp)
[239]1466               
[77]1467                if not m:
[416]1468                        self.logger.debug('This is not a valid email message format')
[77]1469                        return
[239]1470                       
[477]1471                ## Work around lack of header folding in Python; see http://bugs.python.org/issue4696
1472                #
[316]1473                try:
1474                        m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
1475                except AttributeError, detail:
1476                        pass
[239]1477
[408]1478                if self.parameters.debug:         # save the entire e-mail message text
[219]1479                        self.save_email_for_debug(m, True)
[77]1480
1481                self.db = self.env.get_db_cnx()
[194]1482                self.get_sender_info(m)
[152]1483
[221]1484                if not self.email_header_acl('white_list', self.email_addr, True):
[416]1485                        self.logger.info('Message rejected : %s not in white list' %(self.email_addr))
[221]1486                        return False
[77]1487
[221]1488                if self.email_header_acl('black_list', self.email_addr, False):
[416]1489                        self.logger.info('Message rejected : %s in black list' %(self.email_addr))
[221]1490                        return False
1491
[227]1492                if not self.email_header_acl('recipient_list', self.to_email_addr, True):
[416]1493                        self.logger.info('Message rejected : %s not in recipient list' %(self.to_email_addr))
[226]1494                        return False
1495
[477]1496                ## If spam drop the message
[194]1497                #
[204]1498                if self.spam(m) == 'drop':
[194]1499                        return False
1500
[204]1501                elif self.spam(m) == 'spam':
1502                        spam_msg = True
1503                else:
1504                        spam_msg = False
1505
[359]1506                if not m['Subject']:
1507                        subject  = 'No Subject'
1508                else:
1509                        subject  = self.email_to_unicode(m['Subject'])
[304]1510
[416]1511                self.logger.debug('subject: %s' %subject)
[359]1512
[477]1513                ## [hic] #1529: Re: LRZ
1514                #  [hic] #1529?owner=bas,priority=medium: Re: LRZ
[359]1515                #
1516                ticket_regex = r'''
1517                        (?P<new_fields>[#][?].*)
[390]1518                        |(?P<reply>(?P<id>[#][\d]+)(?P<fields>\?.*)?:)
[359]1519                        '''
[477]1520                ## Check if  FullBlogPlugin is installed
[77]1521                #
[260]1522                blog_enabled = None
[359]1523                blog_regex = ''
[260]1524                if self.get_config('components', 'tracfullblog.*') in ['enabled']:
1525                        blog_enabled = True
[359]1526                        blog_regex = '''|(?P<blog>blog:(?P<blog_id>\w*))'''
[329]1527
[77]1528
[477]1529                ## Check if DiscussionPlugin is installed
[260]1530                #
[359]1531                discussion_enabled = None
1532                discussion_regex = ''
1533                if self.get_config('components', 'tracdiscussion.api.*') in ['enabled']:
1534                        discussion_enabled = True
1535                        discussion_regex = r'''
1536                        |(?P<forum>Forum[ ][#](?P<forum_id>\d+)[ ]-[ ]?)
1537                        |(?P<topic>Topic[ ][#](?P<topic_id>\d+)[ ]-[ ]?)
1538                        |(?P<message>Message[ ][#](?P<message_id>\d+)[ ]-[ ]?)
1539                        '''
[77]1540
[359]1541
1542                regex_str = ticket_regex + blog_regex + discussion_regex
1543                SYSTEM_RE = re.compile(regex_str, re.VERBOSE)
1544
[477]1545                ## Find out if this is a ticket, a blog or a discussion
[265]1546                #
[359]1547                result =  SYSTEM_RE.search(subject)
[390]1548
[260]1549                if result:
[477]1550                        ## update ticket + fields
[260]1551                        #
[456]1552                        if result.group('reply'):
[359]1553                                self.system = 'ticket'
[260]1554
[477]1555                                ## Skip the last ':' character
[390]1556                                #
1557                                if not self.ticket_update(m, result.group('reply')[:-1], spam_msg):
1558                                        self.new_ticket(m, subject, spam_msg)
1559
[477]1560                        ## New ticket + fields
[262]1561                        #
1562                        elif result.group('new_fields'):
[359]1563                                self.system = 'ticket'
[262]1564                                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
1565
[359]1566                        if blog_enabled:
1567                                if result.group('blog'):
1568                                        self.system = 'blog'
1569                                        self.blog(result.group('blog_id'))
1570
1571                        if discussion_enabled:
[477]1572                                ## New topic.
[359]1573                                #
1574                                if result.group('forum'):
1575                                        self.system = 'discussion'
1576                                        self.id = int(result.group('forum_id'))
1577                                        self.discussion_topic(m, subject[result.end('forum'):])
1578
[477]1579                                ## Reply to topic.
[359]1580                                #
1581                                elif result.group('topic'):
1582                                        self.system = 'discussion'
1583                                        self.id = int(result.group('topic_id'))
1584                                        self.discussion_topic_reply(m, subject[result.end('topic'):])
1585
[477]1586                                ## Reply to topic message.
[359]1587                                #
1588                                elif result.group('message'):
1589                                        self.system = 'discussion'
1590                                        self.id = int(result.group('message_id'))
1591                                        self.discussion_message_reply(m, subject[result.end('message'):])
1592
[260]1593                else:
[359]1594                        self.system = 'ticket'
[356]1595                        result = self.ticket_update_by_subject(subject)
1596                        if result:
[390]1597                                if not self.ticket_update(m, result, spam_msg):
1598                                        self.new_ticket(m, subject, spam_msg)
[353]1599                        else:
[477]1600                                ## No update by subject, so just create a new ticket
1601                                #
[353]1602                                self.new_ticket(m, subject, spam_msg)
1603
[356]1604
[343]1605########## BODY TEXT functions  ###########################################################
1606
[136]1607        def strip_signature(self, text):
1608                """
1609                Strip signature from message, inspired by Mailman software
1610                """
[462]1611                self.logger.debug('function strip_signature')
1612
[136]1613                body = []
1614                for line in text.splitlines():
1615                        if line == '-- ':
1616                                break
1617                        body.append(line)
1618
1619                return ('\n'.join(body))
1620
[231]1621        def reflow(self, text, delsp = 0):
1622                """
1623                Reflow the message based on the format="flowed" specification (RFC 3676)
1624                """
1625                flowedlines = []
1626                quotelevel = 0
1627                prevflowed = 0
1628
1629                for line in text.splitlines():
1630                        from re import match
1631                       
[477]1632                        ## Figure out the quote level and the content of the current line
1633                        #
[231]1634                        m = match('(>*)( ?)(.*)', line)
1635                        linequotelevel = len(m.group(1))
1636                        line = m.group(3)
1637
[477]1638                        ## Determine whether this line is flowed
1639                        #
[231]1640                        if line and line != '-- ' and line[-1] == ' ':
1641                                flowed = 1
1642                        else:
1643                                flowed = 0
1644
1645                        if flowed and delsp and line and line[-1] == ' ':
1646                                line = line[:-1]
1647
[477]1648                        ## If the previous line is flowed, append this line to it
1649                        #
[231]1650                        if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1651                                flowedlines[-1] += line
[477]1652
1653                        ## Otherwise, start a new line
1654                        #
[231]1655                        else:
1656                                flowedlines.append('>' * linequotelevel + line)
1657
1658                        prevflowed = flowed
1659                       
1660
1661                return '\n'.join(flowedlines)
1662
[191]1663        def strip_quotes(self, text):
[193]1664                """
1665                Strip quotes from message by Nicolas Mendoza
1666                """
[462]1667                self.logger.debug('function strip_quotes')
1668
[193]1669                body = []
1670                for line in text.splitlines():
[462]1671                        try:
1672
1673                                if line.startswith(self.parameters.email_quote):
1674                                        continue
1675
1676                        except UnicodeDecodeError:
1677
1678                                tmp_line = self.email_to_unicode(line)
1679                                if tmp_line.startswith(self.parameters.email_quote):
1680                                        continue
1681                               
[193]1682                        body.append(line)
[151]1683
[193]1684                return ('\n'.join(body))
[191]1685
[309]1686        def inline_properties(self, text):
1687                """
1688                Parse text if we use inline keywords to set ticket fields
1689                """
[409]1690                self.logger.debug('function inline_properties')
[309]1691
1692                properties = dict()
1693                body = list()
1694
1695                INLINE_EXP = re.compile('\s*[@]\s*([a-zA-Z]+)\s*:(.*)$')
1696
1697                for line in text.splitlines():
1698                        match = INLINE_EXP.match(line)
1699                        if match:
1700                                keyword, value = match.groups()
1701                                self.properties[keyword] = value.strip()
[408]1702
[416]1703                                self.logger.debug('inline properties: %s : %s' %(keyword,value))
1704
[309]1705                        else:
1706                                body.append(line)
1707                               
1708                return '\n'.join(body)
1709
1710
[154]1711        def wrap_text(self, text, replace_whitespace = False):
[151]1712                """
[191]1713                Will break a lines longer then given length into several small
1714                lines of size given length
[151]1715                """
1716                import textwrap
[154]1717
[151]1718                LINESEPARATOR = '\n'
[153]1719                reformat = ''
[151]1720
[154]1721                for s in text.split(LINESEPARATOR):
[449]1722                        tmp = textwrap.fill(s, self.parameters.use_textwrap)
[154]1723                        if tmp:
1724                                reformat = '%s\n%s' %(reformat,tmp)
1725                        else:
1726                                reformat = '%s\n' %reformat
[153]1727
1728                return reformat
1729
[154]1730                # Python2.4 and higher
1731                #
1732                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1733                #
1734
[343]1735########## EMAIL attachements functions ###########################################################
1736
[340]1737        def inline_part(self, part):
1738                """
1739                """
[409]1740                self.logger.debug('function inline_part()')
[154]1741
[340]1742                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1743
[236]1744        def get_message_parts(self, msg):
[45]1745                """
[236]1746                parses the email message and returns a list of body parts and attachments
1747                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
[45]1748                """
[409]1749                self.logger.debug('function get_message_parts()')
[317]1750
[309]1751                message_parts = list()
[294]1752       
[278]1753                ALTERNATIVE_MULTIPART = False
1754
[22]1755                for part in msg.walk():
[416]1756                        self.logger.debug('Message part: Main-Type: %s' % part.get_content_maintype())
1757                        self.logger.debug('Message part: Content-Type: %s' % part.get_content_type())
[278]1758
1759                        ## Check content type
[294]1760                        #
1761                        if part.get_content_type() in self.STRIP_CONTENT_TYPES:
[416]1762                                self.logger.debug("A %s attachment named '%s' was skipped" %(part.get_content_type(), part.get_filename()))
[238]1763                                continue
1764
[294]1765                        ## Catch some mulitpart execptions
1766                        #
1767                        if part.get_content_type() == 'multipart/alternative':
[278]1768                                ALTERNATIVE_MULTIPART = True
1769                                continue
1770
[294]1771                        ## Skip multipart containers
[278]1772                        #
[45]1773                        if part.get_content_maintype() == 'multipart':
[416]1774                                self.logger.debug("Skipping multipart container")
1775
[22]1776                                continue
[278]1777                       
[294]1778                        ## Check if this is an inline part. It's inline if there is co Cont-Disp header, or if there is one and it says "inline"
1779                        #
[236]1780                        inline = self.inline_part(part)
1781
[294]1782                        ## Drop HTML message
1783                        #
[447]1784                        if ALTERNATIVE_MULTIPART and self.parameters.drop_alternative_html_version:
[278]1785                                if part.get_content_type() == 'text/html':
[416]1786                                        self.logger.debug('Skipping alternative HTML message')
1787                                        ALTERNATIVE_MULTIPART = False
[278]1788                                        continue
1789
[294]1790                        ## Inline text parts are where the body is
1791                        #
[236]1792                        if part.get_content_type() == 'text/plain' and inline:
[416]1793                                self.logger.debug('               Inline body part')
[236]1794
[477]1795                                ## Try to decode, if fails then do not decode
[45]1796                                #
[90]1797                                body_text = part.get_payload(decode=1)
[45]1798                                if not body_text:                       
[90]1799                                        body_text = part.get_payload(decode=0)
[231]1800
[232]1801                                format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1802                                delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
[231]1803
[447]1804                                if self.parameters.reflow and not self.parameters.verbatim_format and format == 'flowed':
[231]1805                                        body_text = self.reflow(body_text, delsp == 'yes')
[154]1806       
[448]1807                                if self.parameters.strip_signature:
[136]1808                                        body_text = self.strip_signature(body_text)
[22]1809
[448]1810                                if self.parameters.strip_quotes:
[191]1811                                        body_text = self.strip_quotes(body_text)
1812
[449]1813                                if self.parameters.inline_properties:
[309]1814                                        body_text = self.inline_properties(body_text)
1815
[449]1816                                if self.parameters.use_textwrap:
[151]1817                                        body_text = self.wrap_text(body_text)
[148]1818
[294]1819                                ## Get contents charset (iso-8859-15 if not defined in mail headers)
[45]1820                                #
[100]1821                                charset = part.get_content_charset()
[102]1822                                if not charset:
1823                                        charset = 'iso-8859-15'
1824
[89]1825                                try:
[96]1826                                        ubody_text = unicode(body_text, charset)
[100]1827
1828                                except UnicodeError, detail:
[96]1829                                        ubody_text = unicode(body_text, 'iso-8859-15')
[89]1830
[100]1831                                except LookupError, detail:
[139]1832                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
[100]1833
[446]1834                                if self.parameters.verbatim_format:
[236]1835                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1836                                else:
1837                                        message_parts.append('%s' %ubody_text)
1838                        else:
[408]1839                                if self.parameters.debug:
[433]1840                                        s = '              Filename: %s' % part.get_filename()
[379]1841                                        self.print_unicode(s)
[22]1842
[477]1843                                ## First try to use email header function to convert filename.
[383]1844                                #  If this fails the use the plan filename
[477]1845                                #
[383]1846                                try:
1847                                        filename = self.email_to_unicode(part.get_filename())
1848                                except UnicodeEncodeError, detail:
1849                                        filename = part.get_filename()
1850
[317]1851                                message_parts.append((filename, part))
[236]1852
1853                return message_parts
1854               
[253]1855        def unique_attachment_names(self, message_parts):
[296]1856                """
1857                """
[236]1858                renamed_parts = []
1859                attachment_names = set()
[296]1860
[331]1861                for item in message_parts:
[236]1862                       
[296]1863                        ## If not an attachment, leave it alone
1864                        #
[331]1865                        if not isinstance(item, tuple):
1866                                renamed_parts.append(item)
[236]1867                                continue
1868                               
[331]1869                        (filename, part) = item
[295]1870
[296]1871                        ## If no filename, use a default one
1872                        #
1873                        if not filename:
[236]1874                                filename = 'untitled-part'
[22]1875
[477]1876                                ## Guess the extension from the content type, use non strict mode
1877                                #  some additional non-standard but commonly used MIME types
1878                                #  are also recognized
[242]1879                                #
1880                                ext = mimetypes.guess_extension(part.get_content_type(), False)
[236]1881                                if not ext:
1882                                        ext = '.bin'
[22]1883
[236]1884                                filename = '%s%s' % (filename, ext)
[22]1885
[348]1886                        ## Discard relative paths for windows/unix in attachment names
[296]1887                        #
[348]1888                        #filename = filename.replace('\\', '/').replace(':', '/')
1889                        filename = filename.replace('\\', '_')
1890                        filename = filename.replace('/', '_')
[347]1891
[465]1892                        ## remove linefeed char
[296]1893                        #
[465]1894                        for forbidden_char in ['\r', '\n']:
1895                                filename = filename.replace(forbidden_char,'')
1896
[477]1897                        ## We try to normalize the filename to utf-8 NFC if we can.
1898                        #  Files uploaded from OS X might be in NFD.
1899                        #  Check python version and then try it
[465]1900                        #
[348]1901                        #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
1902                        #       try:
1903                        #               filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
1904                        #       except TypeError:
1905                        #               pass
[100]1906
[477]1907                        ## Make the filename unique for this ticket
1908                        #
[236]1909                        num = 0
1910                        unique_filename = filename
[296]1911                        dummy_filename, ext = os.path.splitext(filename)
[134]1912
[339]1913                        while (unique_filename in attachment_names) or self.attachment_exists(unique_filename):
[236]1914                                num += 1
[296]1915                                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
[236]1916                               
[408]1917                        if self.parameters.debug:
[433]1918                                s = 'Attachment with filename %s will be saved as %s' % (filename, unique_filename)
[379]1919                                self.print_unicode(s)
[100]1920
[236]1921                        attachment_names.add(unique_filename)
1922
1923                        renamed_parts.append((filename, unique_filename, part))
[296]1924       
[236]1925                return renamed_parts
1926                       
1927                       
[253]1928        def attachment_exists(self, filename):
[250]1929
[408]1930                if self.parameters.debug:
[433]1931                        s = 'attachment already exists: Id : %s, Filename : %s' %(self.id, filename)
[379]1932                        self.print_unicode(s)
[250]1933
[477]1934                ## We have no valid ticket id
[250]1935                #
[253]1936                if not self.id:
[236]1937                        return False
[250]1938
[236]1939                try:
[359]1940                        if self.system == 'discussion':
1941                                att = attachment.Attachment(self.env, 'discussion', 'ticket/%s'
1942                                  % (self.id,), filename)
1943                        else:
1944                                att = attachment.Attachment(self.env, 'ticket', self.id,
1945                                  filename)
[236]1946                        return True
[250]1947                except attachment.ResourceNotFound:
[236]1948                        return False
[343]1949
1950########## TRAC Ticket Text ###########################################################
[236]1951                       
1952        def body_text(self, message_parts):
1953                body_text = []
1954               
1955                for part in message_parts:
[477]1956
1957                        ## Plain text part, append it
1958                        #
[236]1959                        if not isinstance(part, tuple):
1960                                body_text.extend(part.strip().splitlines())
1961                                body_text.append("")
1962                                continue
1963                               
1964                        (original, filename, part) = part
1965                        inline = self.inline_part(part)
1966                       
1967                        if part.get_content_maintype() == 'image' and inline:
[359]1968                                if self.system != 'discussion':
1969                                        body_text.append('[[Image(%s)]]' % filename)
[236]1970                                body_text.append("")
1971                        else:
[359]1972                                if self.system != 'discussion':
1973                                        body_text.append('[attachment:"%s"]' % filename)
[236]1974                                body_text.append("")
1975                               
1976                body_text = '\r\n'.join(body_text)
1977                return body_text
1978
[343]1979        def html_mailto_link(self, subject):
1980                """
1981                This function returns a HTML mailto tag with the ticket id and author email address
1982                """
1983                if not self.author:
1984                        author = self.email_addr
1985                else:   
1986                        author = self.author
1987
[434]1988                if not self.parameters.mailto_cc:
1989                        self.parameters.mailto_cc = ''
1990
[477]1991                ## use urllib to escape the chars
[343]1992                #
1993                s = 'mailto:%s?Subject=%s&Cc=%s' %(
1994                       urllib.quote(self.email_addr),
1995                           urllib.quote('Re: #%s: %s' %(self.id, subject)),
[434]1996                           urllib.quote(self.parameters.mailto_cc)
[343]1997                           )
1998
1999                s = '\r\n{{{\r\n#!html\r\n<a\r\n href="%s">Reply to: %s\r\n</a>\r\n}}}\r\n' %(s, author)
2000                return s
2001
2002########## TRAC notify section ###########################################################
2003
[253]2004        def notify(self, tkt, new=True, modtime=0):
[79]2005                """
2006                A wrapper for the TRAC notify function. So we can use templates
2007                """
[409]2008                self.logger.debug('function notify()')
[392]2009
[407]2010                if self.parameters.dry_run:
[344]2011                                print 'DRY_RUN: self.notify(tkt, True) reporter = %s' %tkt['reporter']
[250]2012                                return
[41]2013                try:
[392]2014
2015                        #from trac.ticket.web_ui import TicketModule
2016                        #from trac.ticket.notification import TicketNotificationSystem
2017                        #ticket_sys = TicketNotificationSystem(self.env)
2018                        #a = TicketModule(self.env)
2019                        #print a.__dict__
2020                        #tn_sys = TicketNotificationSystem(self.env)
2021                        #print tn_sys
2022                        #print tn_sys.__dict__
2023                        #sys.exit(0)
2024
[477]2025                        ## create false {abs_}href properties, to trick Notify()
[41]2026                        #
[369]2027                        if not (self.VERSION in [0.11, 0.12]):
[192]2028                                self.env.abs_href = Href(self.get_config('project', 'url'))
2029                                self.env.href = Href(self.get_config('project', 'url'))
[22]2030
[392]2031
[41]2032                        tn = TicketNotifyEmail(self.env)
[213]2033
[438]2034                        if self.parameters.alternate_notify_template:
[222]2035
[388]2036                                if self.VERSION >= 0.11:
[222]2037
[221]2038                                        from trac.web.chrome import Chrome
[222]2039
[438]2040                                        if  self.parameters.alternate_notify_template_update and not new:
2041                                                tn.template_name = self.parameters.alternate_notify_template_update
[222]2042                                        else:
[438]2043                                                tn.template_name = self.parameters.alternate_notify_template
[222]2044
[221]2045                                        tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
2046                                               
2047                                else:
[222]2048
[438]2049                                        tn.template_name = self.parameters.alternate_notify_template
[42]2050
[77]2051                        tn.notify(tkt, new, modtime)
[41]2052
2053                except Exception, e:
[431]2054                        self.logger.error('Failure sending notification on creation of ticket #%s: %s' %(self.id, e))
[41]2055
[22]2056
[74]2057
[343]2058########## Parse Config File  ###########################################################
[22]2059
2060def ReadConfig(file, name):
2061        """
2062        Parse the config file
2063        """
2064        if not os.path.isfile(file):
[79]2065                print 'File %s does not exist' %file
[22]2066                sys.exit(1)
2067
[199]2068        config = trac_config.Configuration(file)
[22]2069
[477]2070        ## Use given project name else use defaults
[22]2071        #
2072        if name:
[199]2073                sections = config.sections()
2074                if not name in sections:
[79]2075                        print "Not a valid project name: %s" %name
[199]2076                        print "Valid names: %s" %sections
[22]2077                        sys.exit(1)
2078
[404]2079                project =  SaraDict()
[199]2080                for option, value in  config.options(name):
[452]2081                        try:
2082                                project[option] = int(value)
2083                        except ValueError:
2084                                project[option] = value
[22]2085
2086        else:
[477]2087                ## use some trac internals to get the defaults
[217]2088                #
[404]2089                tmp = config.parser.defaults()
2090                project =  SaraDict()
[22]2091
[452]2092                for option, value in tmp.items():
2093                        try:
2094                                project[option] = int(value)
2095                        except ValueError:
2096                                project[option] = value
[404]2097
[22]2098        return project
2099
[410]2100########## Setup Logging ###############################################################
[87]2101
[419]2102def setup_log(parameters, project_name, interactive=None):
[410]2103        """
2104        Setup loging
2105
[420]2106        Note for log format the usage of `$(...)s` instead of `%(...)s` as the latter form
2107    would be interpreted by the ConfigParser itself.
[410]2108        """
[419]2109        logger = logging.getLogger('email2trac %s' %project_name)
[410]2110
[414]2111        if interactive:
2112                parameters.log_type = 'stderr'
2113
[410]2114        if not parameters.log_type:
2115                parameters.log_type = 'syslog'
2116
2117        if parameters.log_type == 'file':
[415]2118
2119                if not parameters.log_file:
2120                        parameters.log_file = 'email2trac.log'
2121
[410]2122                if not os.path.isabs(parameters.log_file):
[415]2123                        import tempfile
2124                        parameters.log_file = os.path.join(tempfile.gettempdir(), parameters.log_file)
[410]2125
[415]2126                log_handler = logging.FileHandler(parameters.log_file)
2127
[410]2128        elif parameters.log_type in ('winlog', 'eventlog', 'nteventlog'):
[477]2129                ## Requires win32 extensions
2130                #
[410]2131                log_handler = logging.handlers.NTEventLogHandler(logid, logtype='Application')
2132
2133        elif parameters.log_type in ('syslog', 'unix'):
2134                log_handler = logging.handlers.SysLogHandler('/dev/log')
2135
2136        elif parameters.log_type in ('stderr'):
2137                log_handler = logging.StreamHandler(sys.stderr)
2138
2139        else:
2140                log_handler = logging.handlers.BufferingHandler(0)
2141
2142        if parameters.log_format:
2143                parameters.log_format = parameters.log_format.replace('$(', '%(')
2144        else:
[419]2145                parameters.log_format = '%(name)s: %(message)s'
[410]2146
2147        log_formatter = logging.Formatter(parameters.log_format)
2148        log_handler.setFormatter(log_formatter)
2149        logger.addHandler(log_handler)
2150
2151        if (parameters.log_level in ['DEBUG', 'ALL']) or (parameters.debug > 0):
2152                logger.setLevel(logging.DEBUG)
2153
2154        elif parameters.log_level in ['INFO'] or parameters.verbose:
2155                logger.setLevel(logging.INFO)
2156
2157        elif parameters.log_level in ['WARNING']:
2158                logger.setLevel(logging.WARNING)
2159
2160        elif parameters.log_level in ['ERROR']:
2161                logger.setLevel(logging.ERROR)
2162
2163        elif parameters.log_level in ['CRITICAL']:
2164                logger.setLevel(logging.CRITICAL)
2165
2166        else:
2167                logger.setLevel(logging.INFO)
2168
2169        return logger
2170
2171
[22]2172if __name__ == '__main__':
[477]2173        ## Default config file
[22]2174        #
[24]2175        configfile = '@email2trac_conf@'
[22]2176        project = ''
2177        component = ''
[202]2178        ticket_prefix = 'default'
[204]2179        dry_run = None
[317]2180        verbose = None
[414]2181        debug_interactive = None
[202]2182
[412]2183        SHORT_OPT = 'cdhf:np:t:v'
2184        LONG_OPT  =  ['component=', 'debug', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'verbose']
[201]2185
[22]2186        try:
[201]2187                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
[22]2188        except getopt.error,detail:
2189                print __doc__
2190                print detail
2191                sys.exit(1)
[87]2192       
[22]2193        project_name = None
2194        for opt,value in opts:
2195                if opt in [ '-h', '--help']:
2196                        print __doc__
2197                        sys.exit(0)
2198                elif opt in ['-c', '--component']:
2199                        component = value
[412]2200                elif opt in ['-d', '--debug']:
[414]2201                        debug_interactive = 1
[22]2202                elif opt in ['-f', '--file']:
2203                        configfile = value
[201]2204                elif opt in ['-n', '--dry-run']:
[204]2205                        dry_run = True
[22]2206                elif opt in ['-p', '--project']:
2207                        project_name = value
[202]2208                elif opt in ['-t', '--ticket_prefix']:
2209                        ticket_prefix = value
[388]2210                elif opt in ['-v', '--verbose']:
[317]2211                        verbose = True
[87]2212       
[22]2213        settings = ReadConfig(configfile, project_name)
[410]2214
[477]2215        ## The default prefix for ticket values in email2trac.conf
[410]2216        #
[412]2217        settings.ticket_prefix = ticket_prefix
2218        settings.dry_run = dry_run
2219        settings.verbose = verbose
[410]2220
[414]2221        if not settings.debug and debug_interactive:
2222                settings.debug = debug_interactive
[412]2223
[419]2224        if not settings.project:
[22]2225                print __doc__
[79]2226                print 'No Trac project is defined in the email2trac config file.'
[22]2227                sys.exit(1)
[419]2228
2229        logger = setup_log(settings, os.path.basename(settings.project), debug_interactive)
[87]2230       
[22]2231        if component:
2232                settings['component'] = component
[202]2233
[477]2234        ## Determine major trac version used to be in email2trac.conf
[373]2235        # Quick hack for 0.12
[363]2236        #
2237        version = '0.%s' %(trac_version.split('.')[1])
[373]2238        if version.startswith('0.12'):
2239                version = '0.12'
2240
[410]2241        logger.debug("Found trac version: %s" %(version))
[363]2242       
[87]2243        try:
[401]2244                if version == '0.10':
[87]2245                        from trac import attachment
2246                        from trac.env import Environment
2247                        from trac.ticket import Ticket
2248                        from trac.web.href import Href
2249                        from trac import util
[139]2250                        #
2251                        # return  util.text.to_unicode(str)
2252                        #
[87]2253                        # see http://projects.edgewall.com/trac/changeset/2799
2254                        from trac.ticket.notification import TicketNotifyEmail
[199]2255                        from trac import config as trac_config
[359]2256                        from trac.core import TracError
2257
[189]2258                elif version == '0.11':
[182]2259                        from trac import attachment
2260                        from trac.env import Environment
2261                        from trac.ticket import Ticket
2262                        from trac.web.href import Href
[199]2263                        from trac import config as trac_config
[182]2264                        from trac import util
[359]2265                        from trac.core import TracError
[397]2266                        from trac.perm import PermissionSystem
[260]2267
[368]2268                        #
2269                        # return  util.text.to_unicode(str)
2270                        #
2271                        # see http://projects.edgewall.com/trac/changeset/2799
2272                        from trac.ticket.notification import TicketNotifyEmail
[260]2273
[368]2274                elif version == '0.12':
2275                        from trac import attachment
2276                        from trac.env import Environment
2277                        from trac.ticket import Ticket
2278                        from trac.web.href import Href
2279                        from trac import config as trac_config
2280                        from trac import util
2281                        from trac.core import TracError
[397]2282                        from trac.perm import PermissionSystem
[368]2283
[182]2284                        #
2285                        # return  util.text.to_unicode(str)
2286                        #
2287                        # see http://projects.edgewall.com/trac/changeset/2799
2288                        from trac.ticket.notification import TicketNotifyEmail
[368]2289
2290
[189]2291                else:
[410]2292                        logger.error('TRAC version %s is not supported' %version)
[189]2293                        sys.exit(1)
[182]2294
[477]2295                ## Must be set before environment is created
[291]2296                #
2297                if settings.has_key('python_egg_cache'):
2298                        python_egg_cache = str(settings['python_egg_cache'])
2299                        os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
2300
[410]2301                if settings.debug > 0:
2302                        logger.debug('Loading environment %s', settings.project)
[359]2303
[87]2304                env = Environment(settings['project'], create=0)
[333]2305
[410]2306                tktparser = TicketEmailParser(env, settings, logger, float(version))
[87]2307                tktparser.parse(sys.stdin)
[22]2308
[464]2309        ## Catch all errors and use the logging module
[87]2310        #
2311        except Exception, error:
[187]2312
[411]2313                etype, evalue, etb = sys.exc_info()
2314                for e in traceback.format_exception(etype, evalue, etb):
2315                        logger.critical(e)
[187]2316
[97]2317                if m:
[98]2318                        tktparser.save_email_for_debug(m, True)
[97]2319
[249]2320                sys.exit(1)
[22]2321# EOB
Note: See TracBrowser for help on using the repository browser.