source: trunk/email2trac.py.in @ 448

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

converted STRIP_QUOTES and STRIP_SIGNATURES to UserDict?

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