source: trunk/email2trac.py.in @ 395

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

changed some changelogs description.

email2trac.py.in:

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