source: trunk/email2trac.py.in @ 405

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

email2trac.py.in:

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