source: trunk/email2trac.py.in @ 397

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

implement permission system for ticket. Added

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