source: trunk/email2trac.py.in @ 449

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

converteed INLINE_PROPERTIES and USE_TEXTWRAP

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