source: trunk/email2trac.py.in @ 407

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

changed self.DRY_RUN to self.parameters.dry_run

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