source: trunk/email2trac.py.in @ 408

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

port self.DEBUG output to the new logging module

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 60.9 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 408 2010-07-19 15:23:08Z 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
[204]193
[404]194        def setup_log(self):
[402]195                """
[403]196                Setup loging
197
198                Note for log format the usage of `$(...)s` instead of `%(...)s` as the latter form
199        would be interpreted by the ConfigParser itself.
200
[402]201                """
[404]202                if not self.parameters.log_type:
203                        self.parameters.log_type = 'syslog'
[402]204
[404]205                if self.parameters.log_type == 'file':
[403]206                        if not os.path.isabs(parameters.log_file):
[404]207                                self.parameters.log_file = os.path.join(self.env.path, 'log')
[405]208                        self.log_handler = logging.FileHandler()
[403]209
[404]210                elif self.parameters.log_type in ('winlog', 'eventlog', 'nteventlog'):
[403]211                        # Requires win32 extensions
[405]212                        self.log_handler = logging.handlers.NTEventLogHandler(logid, logtype='Application')
[403]213
[404]214                elif self.parameters.log_type in ('syslog', 'unix'):
[405]215                        self.log_handler = logging.handlers.SysLogHandler('/dev/log')
[403]216
[404]217                elif self.parameters.log_type in ('stderr'):
[405]218                        self.log_handler = logging.StreamHandler(sys.stderr)
[403]219
220                else:
[405]221                        self.log_handler = logging.handlers.BufferingHandler(0)
[403]222
[408]223
[404]224                if self.parameters.log_format:
[405]225                        self.parameters.log_format = self.parameters.log_format.replace('$(', '%(')
[403]226                else:
[404]227                        self.parameters.log_format = 'Email2trac: %(message)s'
[403]228
229                self.logger = logging.getLogger('email2trac')
230
231                ## Setup debug level
[405]232                #
233                if not self.parameters.log_level:
234                        self.parameters.log_level = 'CRITICAL'
235                else:
236                        self.parameters.log_level = self.parameters.log_level.upper()
237
[408]238                if self.parameters.log_level in ['DEBUG', 'ALL'] and self.parameters.debug > 0:
[405]239                        self.logger.setLevel(logging.DEBUG)
[406]240
241                elif self.parameters.log_level in ['INFO'] or self.parameters.verbose:
[405]242                        self.logger.setLevel(logging.INFO)
[406]243
[407]244                elif self.parameters.log_level in ['WARNING']:
245                        self.logger.setLevel(logging.WARNING)
246
[406]247                elif self.parameters.log_level in ['ERROR']:
[405]248                        self.logger.setLevel(logging.ERROR)
[406]249
250                elif self.parameters.log_level in ['CRITICAL']:
[405]251                        self.logger.setLevel(logging.CRITICAL)
[406]252
[405]253                else:
[407]254                        self.logger.setLevel(logging.ERROR)
[405]255
256                self.log_formatter = logging.Formatter(self.parameters.log_format)
257                self.log_handler.setFormatter(self.log_formatter)
[406]258
[405]259                self.logger.addHandler(self.log_handler)
260                       
[403]261               
[404]262        def setup_parameters(self):
[402]263                if self.parameters.has_key('umask'):
264                        os.umask(int(self.parameters['umask'], 8))
265
[408]266                if self.parameters.debug:
267                        self.parameters.debug = int(self.parameters.debug)
[22]268                else:
[408]269                        self.parameters.debug = 0
[22]270
[402]271                if self.parameters.has_key('mailto_link'):
272                        self.MAILTO = int(self.parameters['mailto_link'])
273                        if self.parameters.has_key('mailto_cc'):
274                                self.MAILTO_CC = self.parameters['mailto_cc']
[74]275                        else:
276                                self.MAILTO_CC = ''
[22]277                else:
278                        self.MAILTO = 0
279
[402]280                if self.parameters.has_key('spam_level'):
281                        self.SPAM_LEVEL = int(self.parameters['spam_level'])
[22]282                else:
283                        self.SPAM_LEVEL = 0
284
[402]285                if self.parameters.has_key('spam_header'):
286                        self.SPAM_HEADER = self.parameters['spam_header']
[207]287                else:
288                        self.SPAM_HEADER = 'X-Spam-Score'
289
[402]290                if self.parameters.has_key('email_quote'):
291                        self.EMAIL_QUOTE = str(self.parameters['email_quote'])
[191]292                else:   
293                        self.EMAIL_QUOTE = '> '
[22]294
[402]295                if self.parameters.has_key('email_header'):
296                        self.EMAIL_HEADER = int(self.parameters['email_header'])
[22]297                else:
298                        self.EMAIL_HEADER = 0
299
[402]300                if self.parameters.has_key('alternate_notify_template'):
301                        self.notify_template = str(self.parameters['alternate_notify_template'])
[42]302                else:
303                        self.notify_template = None
[22]304
[402]305                if self.parameters.has_key('alternate_notify_template_update'):
306                        self.notify_template_update = str(self.parameters['alternate_notify_template_update'])
[222]307                else:
308                        self.notify_template_update = None
309
[402]310                if self.parameters.has_key('reply_all'):
311                        self.REPLY_ALL = int(self.parameters['reply_all'])
[43]312                else:
313                        self.REPLY_ALL = 0
[42]314
[402]315                if self.parameters.has_key('ticket_permission_system'):
316                        self.TICKET_PERMISSION_SYSTEM = str(self.parameters['ticket_permission_system'])
[397]317                else:
318                        self.TICKET_PERMISSION_SYSTEM = None
319
[402]320                if self.parameters.has_key('ticket_update'):
321                        self.TICKET_UPDATE = int(self.parameters['ticket_update'])
[74]322                else:
323                        self.TICKET_UPDATE = 0
[43]324
[402]325                if self.parameters.has_key('ticket_update_by_subject'):
326                        self.TICKET_UPDATE_BY_SUBJECT = int(self.parameters['ticket_update_by_subject'])
[353]327                else:
328                        self.TICKET_UPDATE_BY_SUBJECT = 0
329
[402]330                if self.parameters.has_key('ticket_update_by_subject_lookback'):
331                        self.TICKET_UPDATE_BY_SUBJECT_LOOKBACK = int(self.parameters['ticket_update_by_subject_lookback'])
[360]332                else:
333                        self.TICKET_UPDATE_BY_SUBJECT_LOOKBACK = 30
334
[402]335                if self.parameters.has_key('drop_spam'):
336                        self.DROP_SPAM = int(self.parameters['drop_spam'])
[118]337                else:
338                        self.DROP_SPAM = 0
[74]339
[402]340                if self.parameters.has_key('verbatim_format'):
341                        self.VERBATIM_FORMAT = int(self.parameters['verbatim_format'])
[134]342                else:
343                        self.VERBATIM_FORMAT = 1
[118]344
[402]345                if self.parameters.has_key('reflow'):
346                        self.REFLOW = int(self.parameters['reflow'])
[231]347                else:
348                        self.REFLOW = 1
349
[402]350                if self.parameters.has_key('drop_alternative_html_version'):
351                        self.DROP_ALTERNATIVE_HTML_VERSION = int(self.parameters['drop_alternative_html_version'])
[278]352                else:
353                        self.DROP_ALTERNATIVE_HTML_VERSION = 0
354
[402]355                if self.parameters.has_key('strip_signature'):
356                        self.STRIP_SIGNATURE = int(self.parameters['strip_signature'])
[136]357                else:
358                        self.STRIP_SIGNATURE = 0
[134]359
[402]360                if self.parameters.has_key('strip_quotes'):
361                        self.STRIP_QUOTES = int(self.parameters['strip_quotes'])
[191]362                else:
363                        self.STRIP_QUOTES = 0
364
[309]365                self.properties = dict()
[402]366                if self.parameters.has_key('inline_properties'):
367                        self.INLINE_PROPERTIES = int(self.parameters['inline_properties'])
[309]368                else:
369                        self.INLINE_PROPERTIES = 0
370
[402]371                if self.parameters.has_key('use_textwrap'):
372                        self.USE_TEXTWRAP = int(self.parameters['use_textwrap'])
[148]373                else:
374                        self.USE_TEXTWRAP = 0
375
[402]376                if self.parameters.has_key('binhex'):
[294]377                        self.STRIP_CONTENT_TYPES.append('application/mac-binhex40')
[238]378
[402]379                if self.parameters.has_key('applesingle'):
[294]380                        self.STRIP_CONTENT_TYPES.append('application/applefile')
[238]381
[402]382                if self.parameters.has_key('appledouble'):
[294]383                        self.STRIP_CONTENT_TYPES.append('application/applefile')
[238]384
[402]385                if self.parameters.has_key('strip_content_types'):
386                        items = self.parameters['strip_content_types'].split(',')
[294]387                        for item in items:
388                                self.STRIP_CONTENT_TYPES.append(item.strip())
389
[257]390                self.WORKFLOW = None
[402]391                if self.parameters.has_key('workflow'):
392                        self.WORKFLOW = self.parameters['workflow']
[257]393
[173]394                # Use OS independend functions
395                #
396                self.TMPDIR = os.path.normcase('/tmp')
[402]397                if self.parameters.has_key('tmpdir'):
398                        self.TMPDIR = os.path.normcase(str(self.parameters['tmpdir']))
[173]399
[402]400                if self.parameters.has_key('ignore_trac_user_settings'):
401                        self.IGNORE_TRAC_USER_SETTINGS = int(self.parameters['ignore_trac_user_settings'])
[194]402                else:
403                        self.IGNORE_TRAC_USER_SETTINGS = 0
[191]404
[402]405                if self.parameters.has_key('email_triggers_workflow'):
406                        self.EMAIL_TRIGGERS_WORKFLOW = int(self.parameters['email_triggers_workflow'])
[320]407                else:
408                        self.EMAIL_TRIGGERS_WORKFLOW = 1
409
[402]410                if self.parameters.has_key('subject_field_separator'):
411                        self.SUBJECT_FIELD_SEPARATOR = self.parameters['subject_field_separator'].strip()
[297]412                else:
413                        self.SUBJECT_FIELD_SEPARATOR = '&'
414
[305]415                self.trac_smtp_from = self.get_config('notification', 'smtp_from')
416
[359]417                self.system = None
418
[341]419########## Email Header Functions ###########################################################
[339]420
[22]421        def spam(self, message):
[191]422                """
423                # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
424                # Note if Spam_level then '*' are included
425                """
[194]426                spam = False
[207]427                if message.has_key(self.SPAM_HEADER):
428                        spam_l = string.split(message[self.SPAM_HEADER])
[22]429
[207]430                        try:
431                                number = spam_l[0].count('*')
432                        except IndexError, detail:
433                                number = 0
434                               
[22]435                        if number >= self.SPAM_LEVEL:
[194]436                                spam = True
437                               
[191]438                # treat virus mails as spam
439                #
440                elif message.has_key('X-Virus-found'):                 
[194]441                        spam = True
442
443                # How to handle SPAM messages
444                #
445                if self.DROP_SPAM and spam:
[408]446                        if self.parameters.debug > 2 :
447                                set.logger.debug('Message is a SPAM. Automatic ticket insertion refused (SPAM level > %d)' % self.SPAM_LEVEL)
[194]448
[204]449                        return 'drop'   
[194]450
451                elif spam:
452
[204]453                        return 'Spam'   
[67]454
[194]455                else:
[22]456
[204]457                        return False
[191]458
[221]459        def email_header_acl(self, keyword, header_field, default):
[206]460                """
[221]461                This function wil check if the email address is allowed or denied
462                to send mail to the ticket list
463            """
[206]464                try:
[221]465                        mail_addresses = self.parameters[keyword]
466
467                        # Check if we have an empty string
468                        #
469                        if not mail_addresses:
470                                return default
471
[206]472                except KeyError, detail:
[408]473                        if self.parameters.debug > 2 :
474                                self.logger.debug('%s not defined, all messages are allowed.' %(keyword))
[206]475
[221]476                        return default
[206]477
[221]478                mail_addresses = string.split(mail_addresses, ',')
479
480                for entry in mail_addresses:
[209]481                        entry = entry.strip()
[221]482                        TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
483                        result =  TO_RE.search(header_field)
[208]484                        if result:
485                                return True
[149]486
[208]487                return False
488
[22]489        def email_header_txt(self, m):
[72]490                """
491                Display To and CC addresses in description field
492                """
[288]493                s = ''
[334]494
[213]495                if m['To'] and len(m['To']) > 0:
[288]496                        s = "'''To:''' %s\r\n" %(m['To'])
[22]497                if m['Cc'] and len(m['Cc']) > 0:
[288]498                        s = "%s'''Cc:''' %s\r\n" % (s, m['Cc'])
[22]499
[288]500                return  self.email_to_unicode(s)
[22]501
[138]502
[194]503        def get_sender_info(self, message):
[45]504                """
[72]505                Get the default author name and email address from the message
[226]506                """
[43]507
[226]508                self.email_to = self.email_to_unicode(message['to'])
509                self.to_name, self.to_email_addr = email.Utils.parseaddr (self.email_to)
510
[194]511                self.email_from = self.email_to_unicode(message['from'])
[287]512                self.email_name, self.email_addr  = email.Utils.parseaddr(self.email_from)
[142]513
[304]514                ## Trac can not handle author's name that contains spaces
515                #  and forbid the ticket email address as author field
[194]516
[305]517                if self.email_addr == self.trac_smtp_from:
[395]518                        if self.email_name:
519                                self.author = self.email_name
520                        else:
521                                self.author = "email2trac"
[304]522                else:
523                        self.author = self.email_addr
524
[194]525                if self.IGNORE_TRAC_USER_SETTINGS:
526                        return
527
528                # Is this a registered user, use email address as search key:
529                # result:
530                #   u : login name
531                #   n : Name that the user has set in the settings tab
532                #   e : email address that the user has set in the settings tab
[45]533                #
[194]534                users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
[250]535                        if e and (e.lower() == self.email_addr.lower()) ]
[43]536
[45]537                if len(users) == 1:
[194]538                        self.email_from = users[0][0]
[250]539                        self.author = users[0][0]
[45]540
[72]541        def set_reply_fields(self, ticket, message):
542                """
543                Set all the right fields for a new ticket
544                """
[408]545                self.logger.info('function set_reply_fields')
[72]546
[270]547                ## Only use name or email adress
548                #ticket['reporter'] = self.email_from
549                ticket['reporter'] = self.author
550
551
[45]552                # Put all CC-addresses in ticket CC field
[43]553                #
554                if self.REPLY_ALL:
555
[299]556                        email_cc = ''
557
558                        cc_addrs = email.Utils.getaddresses( message.get_all('cc', []) )
559
560                        if not cc_addrs:
[105]561                                return
[43]562
[299]563                        ## Build a list of forbidden CC addresses
564                        #
[300]565                        #to_addrs = email.Utils.getaddresses( message.get_all('to', []) )
566                        #to_list = list()
567                        #for n,e in to_addrs:
568                        #       to_list.append(e)
[299]569                               
[392]570                        # Always Remove reporter email address from cc-list
[43]571                        #
[392]572                        try:
573                                cc_addrs.remove((self.author, self.email_addr))
574                        except ValueError, detail:
575                                pass
[43]576
[299]577                        for name,addr in cc_addrs:
578               
579                                ## Prevent mail loop
580                                #
[300]581                                #if addr in to_list:
[304]582
583                                if addr == self.trac_smtp_from:
[408]584                                        if self.parameters.debug:
585                                                self.logger.debug("Skipping %s mail address for CC-field" %(addr))
[299]586                                        continue
[43]587
[299]588                                if email_cc:
589                                        email_cc = '%s, %s' %(email_cc, addr)
590                                else:
591                                        email_cc = addr
[96]592
[299]593                        if email_cc:
[408]594                                if self.parameters.debug:
595                                                self.logger.debug('set_reply_fields: %s' %email_cc)
[299]596
597                                ticket['cc'] = self.email_to_unicode(email_cc)
598
[339]599
600########## DEBUG functions  ###########################################################
601
[310]602        def debug_body(self, message_body, tempfile=False):
603                if tempfile:
604                        import tempfile
605                        body_file = tempfile.mktemp('.email2trac')
606                else:
607                        body_file = os.path.join(self.TMPDIR, 'body.txt')
608
[407]609                if self.parameters.dry_run:
[331]610                        print 'DRY-RUN: not saving body to %s' %(body_file)
611                        return
612
613                print 'TD: writing body to %s' %(body_file)
614                fx = open(body_file, 'wb')
[310]615                if not message_body:
[331]616                                message_body = '(None)'
[310]617
618                message_body = message_body.encode('utf-8')
619                #message_body = unicode(message_body, 'iso-8859-15')
620
621                fx.write(message_body)
622                fx.close()
623                try:
624                        os.chmod(body_file,S_IRWXU|S_IRWXG|S_IRWXO)
625                except OSError:
626                        pass
627
628        def debug_attachments(self, message_parts):
[317]629                """
630                """
[406]631                self.logger.info('function debug_attachments')
[317]632               
[310]633                n = 0
[331]634                for item in message_parts:
[310]635                        # Skip inline text parts
[331]636                        if not isinstance(item, tuple):
[310]637                                continue
638                               
[331]639                        (original, filename, part) = item
[310]640
641                        n = n + 1
642                        print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type())
[379]643               
644                        s = 'TD: part%d: filename: %s' %(n, filename)
645                        self.print_unicode(s)
646       
[348]647                        ## Forbidden chars
648                        #
649                        filename = filename.replace('\\', '_')
650                        filename = filename.replace('/', '_')
651       
652
653                        part_file = os.path.join(self.TMPDIR, filename)
[379]654                        s = 'TD: writing part%d (%s)' % (n,part_file)
655                        self.print_unicode(s)
[331]656
[407]657                        if self.parameters.dry_run:
[331]658                                print 'DRY_RUN: NOT saving attachments'
659                                continue
660
[377]661                        part_file = util.text.unicode_quote(part_file)
662
[310]663                        fx = open(part_file, 'wb')
664                        text = part.get_payload(decode=1)
[331]665
[310]666                        if not text:
667                                text = '(None)'
[331]668
[310]669                        fx.write(text)
670                        fx.close()
[331]671
[310]672                        try:
673                                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
674                        except OSError:
675                                pass
676
[96]677        def save_email_for_debug(self, message, tempfile=False):
[309]678
[96]679                if tempfile:
680                        import tempfile
681                        msg_file = tempfile.mktemp('.email2trac')
682                else:
[173]683                        #msg_file = '/var/tmp/msg.txt'
684                        msg_file = os.path.join(self.TMPDIR, 'msg.txt')
685
[407]686                if self.parameters.dry_run:
[331]687                        print 'DRY_RUN: NOT saving email message to %s' %(msg_file)
688                else:
689                        print 'TD: saving email to %s' %(msg_file)
[44]690
[331]691                        fx = open(msg_file, 'wb')
692                        fx.write('%s' % message)
693                        fx.close()
694                       
695                        try:
696                                os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
697                        except OSError:
698                                pass
699
[309]700                message_parts = self.get_message_parts(message)
701                message_parts = self.unique_attachment_names(message_parts)
702                body_text = self.body_text(message_parts)
703                self.debug_body(body_text, True)
704                self.debug_attachments(message_parts)
705
[339]706########## Conversion functions  ###########################################################
707
[341]708        def email_to_unicode(self, message_str):
709                """
710                Email has 7 bit ASCII code, convert it to unicode with the charset
711                that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
712                understands it.
713                """
[406]714                self.logger.info("function email_to_unicode")
[341]715
716                results =  email.Header.decode_header(message_str)
717
718                s = None
719                for text,format in results:
720                        if format:
721                                try:
722                                        temp = unicode(text, format)
723                                except UnicodeError, detail:
724                                        # This always works
725                                        #
726                                        temp = unicode(text, 'iso-8859-15')
727                                except LookupError, detail:
728                                        #text = 'ERROR: Could not find charset: %s, please install' %format
729                                        #temp = unicode(text, 'iso-8859-15')
730                                        temp = message_str
731                                       
732                        else:
733                                temp = string.strip(text)
734                                temp = unicode(text, 'iso-8859-15')
735
736                        if s:
737                                s = '%s %s' %(s, temp)
738                        else:
739                                s = '%s' %temp
740
741                #s = s.encode('utf-8')
742                return s
743
[288]744        def str_to_dict(self, s):
[164]745                """
[288]746                Transfrom a string of the form [<key>=<value>]+ to dict[<key>] = <value>
[164]747                """
[406]748                self.logger.info("function str_to_dict")
[164]749
[297]750                fields = string.split(s, self.SUBJECT_FIELD_SEPARATOR)
[262]751
[164]752                result = dict()
753                for field in fields:
754                        try:
[262]755                                index, value = string.split(field, '=')
[169]756
757                                # We can not change the description of a ticket via the subject
758                                # line. The description is the body of the email
759                                #
760                                if index.lower() in ['description']:
761                                        continue
762
[164]763                                if value:
[165]764                                        result[index.lower()] = value
[169]765
[164]766                        except ValueError:
767                                pass
[165]768                return result
[167]769
[379]770        def print_unicode(self,s):
771                """
772                This function prints unicode strings uif possible else it will quote it
773                """
774                try:
[408]775                        self.logger.debug(s)
[379]776                except UnicodeEncodeError, detail:
[408]777                        self.logger.debug(util.text.unicode_quote(s))
[379]778
[339]779########## TRAC ticket functions  ###########################################################
780
[398]781        def check_permission_participants(self, tkt):
[397]782                """
[398]783                Check if the mailer is allowed to update the ticket
784                """
[408]785                self.logger.info('function check_permission_participants')
[398]786
787                if tkt['reporter'].lower() in [self.author, self.email_addr]:
[408]788                        if self.parameters.debug:
789                                self.logger.debug('ALLOW, %s is the ticket reporter' %(self.email_addr))
790
[398]791                        return True
792
793                perm = PermissionSystem(self.env)
794                if perm.check_permission('TICKET_MODIFY', self.author):
[408]795                        if self.parameters.debug:
796                                self.logger.debug('ALLOW, %s has trac permission to update the ticket' %(self.author))
797
[398]798                        return True
[408]799
[398]800                else:
801                        return False
802               
803
804                # Is the updater in the CC?
805                try:
806                        cc_list = tkt['cc'].split(',')
807                        for cc in cc_list:
808                                if self.email_addr.lower() in cc.strip():
809
[408]810                                        if self.parameters.debug:
811                                                self.logger.debug('ALLOW, %s is in the CC' %(self.email_addr))
[398]812
813                                        return True
814
815                except KeyError:
816                        return False
817
818        def check_permission(self, tkt, action):
819                """
[397]820                check if the reporter has the right permission for the action:
821          - TICKET_CREATE
822          - TICKET_MODIFY
823
824                There are three models:
825                        - None      : no checking at all
826                        - trac      : check the permission via trac permission model
827                        - email2trac: ....
828                """
[406]829                self.logger.info("function check_permission")
[397]830
831                if self.TICKET_PERMISSION_SYSTEM in ['trac']:
[398]832
[397]833                        perm = PermissionSystem(self.env)
834                        if perm.check_permission(action, self.author):
835                                return True
836                        else:
837                                return False
[398]838
839                elif self.TICKET_PERMISSION_SYSTEM in ['update_restricted_to_participants']:
840                        if action in ['TICKET_MODIFY']:
841                                return (self.check_permission_participants(tkt))       
842                        else:
843                                return True
844
845                # Default is to allow everybody ticket updates and ticket creation
[397]846                else:
847                                return True
848
849
[202]850        def update_ticket_fields(self, ticket, user_dict, use_default=None):
851                """
852                This will update the ticket fields. It will check if the
853                given fields are known and if the right values are specified
854                It will only update the ticket field value:
[169]855                        - If the field is known
[202]856                        - If the value supplied is valid for the ticket field.
857                          If not then there are two options:
858                           1) Skip the value (use_default=None)
859                           2) Set default value for field (use_default=1)
[169]860                """
[406]861                self.logger.info("function update_ticket_fields")
[169]862
863                # Build a system dictionary from the ticket fields
864                # with field as index and option as value
865                #
866                sys_dict = dict()
867                for field in ticket.fields:
[167]868                        try:
[169]869                                sys_dict[field['name']] = field['options']
870
[167]871                        except KeyError:
[169]872                                sys_dict[field['name']] = None
[167]873                                pass
[169]874
[301]875                ## Check user supplied fields an compare them with the
[169]876                # system one's
877                #
878                for field,value in user_dict.items():
[408]879                        if self.parameters.debug:
[379]880                                s = 'TD: user_field\t %s = %s' %(field,value)
881                                self.print_unicode(s)
[169]882
[301]883                        ## To prevent mail loop
884                        #
885                        if field == 'cc':
886
887                                cc_list = user_dict['cc'].split(',')
888
[304]889                                if self.trac_smtp_from in cc_list:
[408]890                                        if self.parameters.debug:
891                                                self.logger.debug('TD: MAIL LOOP: %s is not allowed as CC address' %(self.trac_smtp_from))
892
[304]893                                        cc_list.remove(self.trac_smtp_from)
[301]894
895                                value = ','.join(cc_list)
896                               
897
[169]898                        if sys_dict.has_key(field):
899
900                                # Check if value is an allowed system option, if TypeError then
901                                # every value is allowed
902                                #
903                                try:
904                                        if value in sys_dict[field]:
905                                                ticket[field] = value
[202]906                                        else:
907                                                # Must we set a default if value is not allowed
908                                                #
909                                                if use_default:
910                                                        value = self.get_config('ticket', 'default_%s' %(field) )
[169]911
912                                except TypeError:
[345]913                                        pass
914
915                                ## Only set if we have a value
916                                #
917                                if value:
[169]918                                        ticket[field] = value
[202]919
[408]920                                if self.parameters.debug:
[379]921                                        s = 'ticket_field\t %s = %s' %(field,  ticket[field])
922                                        self.print_unicode(s)
[345]923
[260]924        def ticket_update(self, m, id, spam):
[78]925                """
[79]926                If the current email is a reply to an existing ticket, this function
927                will append the contents of this email to that ticket, instead of
928                creating a new one.
[78]929                """
[406]930                self.logger.info("function ticket_update")
[202]931
[164]932                # Must we update ticket fields
933                #
[220]934                update_fields = dict()
[165]935                try:
[260]936                        id, keywords = string.split(id, '?')
[262]937
[220]938                        update_fields = self.str_to_dict(keywords)
[165]939
940                        # Strip '#'
941                        #
[260]942                        self.id = int(id[1:])
[165]943
[260]944                except ValueError:
[390]945
[362]946                        # Strip '#'
[165]947                        #
[362]948                        self.id = int(id[1:])
[164]949
[406]950                self.logger.info("function ticket_update id %s" %id)
[71]951
[194]952                # When is the change committed
953                #
[386]954                if self.VERSION < 0.11:
955                        when = int(time.time())
956                else:
[194]957                        utc = UTC()
958                        when = datetime.now(utc)
[77]959
[172]960                try:
[253]961                        tkt = Ticket(self.env, self.id, self.db)
[390]962
[172]963                except util.TracError, detail:
[390]964
[253]965                        # Not a valid ticket
[390]966
[253]967                        self.id = None
[172]968                        return False
[126]969
[398]970                # Check the permission of the reporter
971                #
972                if self.TICKET_PERMISSION_SYSTEM:
973                        if not self.check_permission(tkt, 'TICKET_MODIFY'):
974                                print 'Reporter: %s has no permission to modify tickets' %self.author
975                                return False
976
[288]977                # How many changes has this ticket
978                cnum = len(tkt.get_changelog())
979
980
[220]981                # reopen the ticket if it is was closed
982                # We must use the ticket workflow framework
983                #
[320]984                if tkt['status'] in ['closed'] and self.EMAIL_TRIGGERS_WORKFLOW:
[220]985
[257]986                        #print controller.actions['reopen']
987                        #
988                        # As reference 
989                        # req = Mock(href=Href('/'), abs_href=Href('http://www.example.com/'), authname='anonymous', perm=MockPerm(), args={})
990                        #
991                        #a = controller.render_ticket_action_control(req, tkt, 'reopen')
992                        #print 'controller : ', a
993                        #
994                        #b = controller.get_all_status()
995                        #print 'get all status: ', b
996                        #
997                        #b = controller.get_ticket_changes(req, tkt, 'reopen')
998                        #print 'get_ticket_changes :', b
999
[388]1000                        if self.WORKFLOW and (self.VERSION >= 0.11 ) :
[257]1001                                from trac.ticket.default_workflow import ConfigurableTicketWorkflow
1002                                from trac.test import Mock, MockPerm
1003
1004                                req = Mock(authname='anonymous', perm=MockPerm(), args={})
1005
1006                                controller = ConfigurableTicketWorkflow(self.env)
1007                                fields = controller.get_ticket_changes(req, tkt, self.WORKFLOW)
1008
[408]1009                                if self.parameters.debug:
1010                                        self.logger.debug('Workflow ticket update fields: ')
[257]1011
1012                                for key in fields.keys():
[408]1013                                        if self.parameters.debug:
1014                                                self.logger.debug('\t %s : %s' %(key, fields[key]))
1015
[257]1016                                        tkt[key] = fields[key]
1017
1018                        else:
1019                                tkt['status'] = 'reopened'
1020                                tkt['resolution'] = ''
1021
[309]1022                # Must we update some ticket fields properties via subjectline
[172]1023                #
[220]1024                if update_fields:
1025                        self.update_ticket_fields(tkt, update_fields)
[166]1026
[236]1027                message_parts = self.get_message_parts(m)
[253]1028                message_parts = self.unique_attachment_names(message_parts)
[210]1029
[309]1030                # Must we update some ticket fields properties via body_text
1031                #
1032                if self.properties:
1033                                self.update_ticket_fields(tkt, self.properties)
1034
[177]1035                if self.EMAIL_HEADER:
[236]1036                        message_parts.insert(0, self.email_header_txt(m))
[76]1037
[236]1038                body_text = self.body_text(message_parts)
1039
[401]1040                error_with_attachments = self.attach_attachments(message_parts)
[348]1041
[309]1042                if body_text.strip() or update_fields or self.properties:
[407]1043                        if self.parameters.dry_run:
[288]1044                                print 'DRY_RUN: tkt.save_changes(self.author, body_text, ticket_change_number) ', self.author, cnum
[250]1045                        else:
[348]1046                                if error_with_attachments:
1047                                        body_text = '%s\\%s' %(error_with_attachments, body_text)
1048                               
[288]1049                                tkt.save_changes(self.author, body_text, when, None, str(cnum))
1050                       
[219]1051
[392]1052                if not spam:
[253]1053                        self.notify(tkt, False, when)
[72]1054
[71]1055                return True
1056
[202]1057        def set_ticket_fields(self, ticket):
[77]1058                """
[202]1059                set the ticket fields to value specified
1060                        - /etc/email2trac.conf with <prefix>_<field>
1061                        - trac default values, trac.ini
1062                """
1063                user_dict = dict()
1064
1065                for field in ticket.fields:
1066
1067                        name = field['name']
1068
[335]1069                        ## default trac value
[202]1070                        #
[233]1071                        if not field.get('custom'):
1072                                value = self.get_config('ticket', 'default_%s' %(name) )
1073                        else:
[345]1074                                ##  Else we get the default value for reporter
1075                                #
[233]1076                                value = field.get('value')
1077                                options = field.get('options')
[345]1078
[335]1079                                if value and options and (value not in options):
[345]1080                                         value = options[int(value)]
1081       
[408]1082                        if self.parameters.debug:
[379]1083                                s = 'TD: trac.ini name %s = %s' %(name, value)
1084                                self.print_unicode(s)
[202]1085
[335]1086                        ## email2trac.conf settings
1087                        #
[206]1088                        prefix = self.parameters['ticket_prefix']
[202]1089                        try:
[206]1090                                value = self.parameters['%s_%s' %(prefix, name)]
[408]1091                                if self.parameters.debug > 10:
[379]1092                                        s = 'TD: email2trac.conf %s = %s ' %(name, value)
1093                                        self.print_unicode(s)
[202]1094
1095                        except KeyError, detail:
1096                                pass
1097               
[408]1098                        if self.parameters.debug:
[379]1099                                s = 'TD: user_dict[%s] = %s' %(name, value)
1100                                self.print_unicode(s)
[202]1101
[345]1102                        if value:
1103                                user_dict[name] = value
[202]1104
1105                self.update_ticket_fields(ticket, user_dict, use_default=1)
1106
[352]1107                if 'status' not in user_dict.keys():
1108                        ticket['status'] = 'new'
[202]1109
1110
[356]1111        def ticket_update_by_subject(self, subject):
1112                """
1113                This list of Re: prefixes is probably incomplete. Taken from
1114                wikipedia. Here is how the subject is matched
1115                  - Re: <subject>
1116                  - Re: (<Mail list label>:)+ <subject>
[202]1117
[356]1118                So we must have the last column
1119                """
[406]1120                self.logger.info('function ticket_update_by_subject')
[356]1121
1122                matched_id = None
1123                if self.TICKET_UPDATE and self.TICKET_UPDATE_BY_SUBJECT:
1124                               
1125                        SUBJECT_RE = re.compile(r'^(RE|AW|VS|SV):(.*:)*\s*(.*)', re.IGNORECASE)
1126                        result = SUBJECT_RE.search(subject)
1127
1128                        if result:
1129                                # This is a reply
1130                                orig_subject = result.group(3)
1131
[408]1132                                if self.parameters.debug:
1133                                        self.logger.debug('subject search string: %s' %(orig_subject))
[356]1134
1135                                cursor = self.db.cursor()
1136                                summaries = [orig_subject, '%%: %s' % orig_subject]
1137
[360]1138                                ##
1139                                # Convert days to seconds
1140                                lookback = int(time.mktime(time.gmtime())) - \
1141                                                self.TICKET_UPDATE_BY_SUBJECT_LOOKBACK * 24 * 3600
[356]1142
[360]1143
[356]1144                                for summary in summaries:
[408]1145                                        if self.parameters.debug:
1146                                                self.logger.debug('Looking for summary matching: "%s"' % summary)
1147
[356]1148                                        sql = """SELECT id FROM ticket
1149                                                        WHERE changetime >= %s AND summary LIKE %s
1150                                                        ORDER BY changetime DESC"""
1151                                        cursor.execute(sql, [lookback, summary.strip()])
1152
1153                                        for row in cursor:
1154                                                (matched_id,) = row
[408]1155
1156                                                if self.parameters.debug:
1157                                                        self.logger.debug('Found matching ticket id: %d' % matched_id)
1158
[356]1159                                                break
1160
1161                                        if matched_id:
[366]1162                                                matched_id = '#%d' % matched_id
[356]1163                                                return matched_id
1164
1165                return matched_id
1166
1167
[262]1168        def new_ticket(self, msg, subject, spam, set_fields = None):
[202]1169                """
[77]1170                Create a new ticket
1171                """
[406]1172                self.logger.info('function new_ticket')
[250]1173
[41]1174                tkt = Ticket(self.env)
[326]1175
1176                self.set_reply_fields(tkt, msg)
1177
[202]1178                self.set_ticket_fields(tkt)
1179
[397]1180                # Check the permission of the reporter
1181                #
1182                if self.TICKET_PERMISSION_SYSTEM:
[398]1183                        if not self.check_permission(tkt, 'TICKET_CREATE'):
[408]1184                                self.logger.error('Reporter: %s has no permission to create tickets' %self.author)
[397]1185                                return False
1186
[202]1187                # Old style setting for component, will be removed
1188                #
[204]1189                if spam:
1190                        tkt['component'] = 'Spam'
1191
[206]1192                elif self.parameters.has_key('component'):
1193                        tkt['component'] = self.parameters['component']
[201]1194
[22]1195                if not msg['Subject']:
[151]1196                        tkt['summary'] = u'(No subject)'
[22]1197                else:
[264]1198                        tkt['summary'] = subject
[22]1199
1200
[262]1201                if set_fields:
1202                        rest, keywords = string.split(set_fields, '?')
1203
1204                        if keywords:
1205                                update_fields = self.str_to_dict(keywords)
1206                                self.update_ticket_fields(tkt, update_fields)
1207
[45]1208                # produce e-mail like header
1209                #
[22]1210                head = ''
1211                if self.EMAIL_HEADER > 0:
1212                        head = self.email_header_txt(msg)
[296]1213
[236]1214                message_parts = self.get_message_parts(msg)
[309]1215
1216                # Must we update some ticket fields properties via body_text
1217                #
1218                if self.properties:
1219                                self.update_ticket_fields(tkt, self.properties)
1220
[236]1221                message_parts = self.unique_attachment_names(message_parts)
1222               
1223                if self.EMAIL_HEADER > 0:
1224                        message_parts.insert(0, self.email_header_txt(msg))
1225                       
1226                body_text = self.body_text(message_parts)
[45]1227
[236]1228                tkt['description'] = body_text
[90]1229
[182]1230                #when = int(time.time())
[192]1231                #
[182]1232                utc = UTC()
1233                when = datetime.now(utc)
[45]1234
[408]1235                if self.parameters.dry_run:
1236                        print 'DRY_RUN: tkt.insert()'
1237                else:
[253]1238                        self.id = tkt.insert()
[273]1239       
[90]1240                changed = False
1241                comment = ''
[77]1242
[273]1243                # some routines in trac are dependend on ticket id     
1244                # like alternate notify template
1245                #
1246                if self.notify_template:
[274]1247                        tkt['id'] = self.id
[273]1248                        changed = True
1249
[295]1250                ## Rewrite the description if we have mailto enabled
[45]1251                #
[72]1252                if self.MAILTO:
[100]1253                        changed = True
[142]1254                        comment = u'\nadded mailto line\n'
[343]1255                        mailto = self.html_mailto_link( m['Subject'])
[253]1256
[213]1257                        tkt['description'] = u'%s\r\n%s%s\r\n' \
[142]1258                                %(head, mailto, body_text)
[295]1259       
1260                ## Save the attachments to the ticket   
1261                #
[340]1262                error_with_attachments =  self.attach_attachments(message_parts)
[295]1263
[319]1264                if error_with_attachments:
1265                        changed = True
1266                        comment = '%s\n%s\n' %(comment, error_with_attachments)
[45]1267
[90]1268                if changed:
[408]1269                        if self.parameters.dry_run:
[344]1270                                print 'DRY_RUN: tkt.save_changes(%s, comment) real reporter = %s' %( tkt['reporter'], self.author)
[201]1271                        else:
[344]1272                                tkt.save_changes(tkt['reporter'], comment)
[201]1273                                #print tkt.get_changelog(self.db, when)
[90]1274
[392]1275                if not spam:
[253]1276                        self.notify(tkt, True)
[45]1277
[260]1278
[342]1279        def attach_attachments(self, message_parts, update=False):
1280                '''
1281                save any attachments as files in the ticket's directory
1282                '''
[406]1283                self.logger.info('function attach_attachments()')
[342]1284
[407]1285                if self.parameters.dry_run:
[342]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
[408]1412                if self.parameters.debug:
1413                        self.logger.debug('Creating a new topic in forum:', self.id)
[359]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
[408]1422                if not forum and self.parameters.debug:
1423                        self.logger.debug("ERROR: Replied forum doesn't exist")
[359]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
[408]1443                if self.parameters.debug:
1444                        self.logger.debug('Replying to discussion topic', self.id)
[359]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
[408]1453                if not topic and self.parameters.debug:
1454                        self.logger.debug("ERROR: Replied topic doesn't exist")
[359]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
[408]1474                if self.parameters.debug:
1475                        self.loggger.debug('TD: Replying to discussion message', self.id)
[359]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
[408]1484                if not message and self.parameters.debug:
1485                        self.logger.debug("ERROR: Replied message doesn't exist")
[359]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')
[96]1601                global m
1602
[77]1603                m = email.message_from_file(fp)
[239]1604               
[77]1605                if not m:
[408]1606                        if self.parameters.debug:
1607                                self.logger.debug('This is not a valid email message format')
1608
[77]1609                        return
[239]1610                       
1611                # Work around lack of header folding in Python; see http://bugs.python.org/issue4696
[316]1612                try:
1613                        m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
1614                except AttributeError, detail:
1615                        pass
[239]1616
[408]1617                if self.parameters.debug:         # save the entire e-mail message text
[219]1618                        self.save_email_for_debug(m, True)
[77]1619
1620                self.db = self.env.get_db_cnx()
[194]1621                self.get_sender_info(m)
[152]1622
[221]1623                if not self.email_header_acl('white_list', self.email_addr, True):
[408]1624                        if self.parameters.debug:
1625                                self.logger.debug('Message rejected : %s not in white list' %(self.email_addr))
1626
[221]1627                        return False
[77]1628
[221]1629                if self.email_header_acl('black_list', self.email_addr, False):
[408]1630                        if self.parameters.debug:
1631                                self.logger.debug('Message rejected : %s in black list' %(self.email_addr))
1632
[221]1633                        return False
1634
[227]1635                if not self.email_header_acl('recipient_list', self.to_email_addr, True):
[408]1636                        if self.parameters.debug:
1637                                self.logger.debug('Message rejected : %s not in recipient list' %(self.to_email_addr))
1638
[226]1639                        return False
1640
[204]1641                # If drop the message
[194]1642                #
[204]1643                if self.spam(m) == 'drop':
[194]1644                        return False
1645
[204]1646                elif self.spam(m) == 'spam':
1647                        spam_msg = True
1648                else:
1649                        spam_msg = False
1650
[359]1651                if not m['Subject']:
1652                        subject  = 'No Subject'
1653                else:
1654                        subject  = self.email_to_unicode(m['Subject'])
[304]1655
[408]1656                if self.parameters.debug:
1657                        self.logger.debug('subject: %s' %subject)
[359]1658
1659                #
1660                # [hic] #1529: Re: LRZ
1661                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
1662                #
1663                ticket_regex = r'''
1664                        (?P<new_fields>[#][?].*)
[390]1665                        |(?P<reply>(?P<id>[#][\d]+)(?P<fields>\?.*)?:)
[359]1666                        '''
[260]1667                # Check if  FullBlogPlugin is installed
[77]1668                #
[260]1669                blog_enabled = None
[359]1670                blog_regex = ''
[260]1671                if self.get_config('components', 'tracfullblog.*') in ['enabled']:
1672                        blog_enabled = True
[359]1673                        blog_regex = '''|(?P<blog>blog:(?P<blog_id>\w*))'''
[329]1674
[77]1675
[359]1676                # Check if DiscussionPlugin is installed
[260]1677                #
[359]1678                discussion_enabled = None
1679                discussion_regex = ''
1680                if self.get_config('components', 'tracdiscussion.api.*') in ['enabled']:
1681                        discussion_enabled = True
1682                        discussion_regex = r'''
1683                        |(?P<forum>Forum[ ][#](?P<forum_id>\d+)[ ]-[ ]?)
1684                        |(?P<topic>Topic[ ][#](?P<topic_id>\d+)[ ]-[ ]?)
1685                        |(?P<message>Message[ ][#](?P<message_id>\d+)[ ]-[ ]?)
1686                        '''
[77]1687
[359]1688
1689                regex_str = ticket_regex + blog_regex + discussion_regex
1690                SYSTEM_RE = re.compile(regex_str, re.VERBOSE)
1691
1692                # Find out if this is a ticket, a blog or a discussion
[265]1693                #
[359]1694                result =  SYSTEM_RE.search(subject)
[390]1695
[260]1696                if result:
1697                        # update ticket + fields
1698                        #
[359]1699                        if result.group('reply') and self.TICKET_UPDATE:
1700                                self.system = 'ticket'
[260]1701
[390]1702                                # Skip the last ':' character
1703                                #
1704                                if not self.ticket_update(m, result.group('reply')[:-1], spam_msg):
1705                                        self.new_ticket(m, subject, spam_msg)
1706
[262]1707                        # New ticket + fields
1708                        #
1709                        elif result.group('new_fields'):
[359]1710                                self.system = 'ticket'
[262]1711                                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
1712
[359]1713                        if blog_enabled:
1714                                if result.group('blog'):
1715                                        self.system = 'blog'
1716                                        self.blog(result.group('blog_id'))
1717
1718                        if discussion_enabled:
1719                                # New topic.
1720                                #
1721                                if result.group('forum'):
1722                                        self.system = 'discussion'
1723                                        self.id = int(result.group('forum_id'))
1724                                        self.discussion_topic(m, subject[result.end('forum'):])
1725
1726                                # Reply to topic.
1727                                #
1728                                elif result.group('topic'):
1729                                        self.system = 'discussion'
1730                                        self.id = int(result.group('topic_id'))
1731                                        self.discussion_topic_reply(m, subject[result.end('topic'):])
1732
1733                                # Reply to topic message.
1734                                #
1735                                elif result.group('message'):
1736                                        self.system = 'discussion'
1737                                        self.id = int(result.group('message_id'))
1738                                        self.discussion_message_reply(m, subject[result.end('message'):])
1739
[260]1740                else:
[359]1741                        self.system = 'ticket'
[356]1742                        result = self.ticket_update_by_subject(subject)
1743                        if result:
[390]1744                                if not self.ticket_update(m, result, spam_msg):
1745                                        self.new_ticket(m, subject, spam_msg)
[353]1746                        else:
1747                                # No update by subject, so just create a new ticket
1748                                self.new_ticket(m, subject, spam_msg)
1749
[356]1750
[343]1751########## BODY TEXT functions  ###########################################################
1752
[136]1753        def strip_signature(self, text):
1754                """
1755                Strip signature from message, inspired by Mailman software
1756                """
1757                body = []
1758                for line in text.splitlines():
1759                        if line == '-- ':
1760                                break
1761                        body.append(line)
1762
1763                return ('\n'.join(body))
1764
[231]1765        def reflow(self, text, delsp = 0):
1766                """
1767                Reflow the message based on the format="flowed" specification (RFC 3676)
1768                """
1769                flowedlines = []
1770                quotelevel = 0
1771                prevflowed = 0
1772
1773                for line in text.splitlines():
1774                        from re import match
1775                       
1776                        # Figure out the quote level and the content of the current line
1777                        m = match('(>*)( ?)(.*)', line)
1778                        linequotelevel = len(m.group(1))
1779                        line = m.group(3)
1780
1781                        # Determine whether this line is flowed
1782                        if line and line != '-- ' and line[-1] == ' ':
1783                                flowed = 1
1784                        else:
1785                                flowed = 0
1786
1787                        if flowed and delsp and line and line[-1] == ' ':
1788                                line = line[:-1]
1789
1790                        # If the previous line is flowed, append this line to it
1791                        if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1792                                flowedlines[-1] += line
1793                        # Otherwise, start a new line
1794                        else:
1795                                flowedlines.append('>' * linequotelevel + line)
1796
1797                        prevflowed = flowed
1798                       
1799
1800                return '\n'.join(flowedlines)
1801
[191]1802        def strip_quotes(self, text):
[193]1803                """
1804                Strip quotes from message by Nicolas Mendoza
1805                """
1806                body = []
1807                for line in text.splitlines():
1808                        if line.startswith(self.EMAIL_QUOTE):
1809                                continue
1810                        body.append(line)
[151]1811
[193]1812                return ('\n'.join(body))
[191]1813
[309]1814        def inline_properties(self, text):
1815                """
1816                Parse text if we use inline keywords to set ticket fields
1817                """
[408]1818                if self.parameters.debug:
1819                        self.logger.info('function inline_properties')
[309]1820
1821                properties = dict()
1822                body = list()
1823
1824                INLINE_EXP = re.compile('\s*[@]\s*([a-zA-Z]+)\s*:(.*)$')
1825
1826                for line in text.splitlines():
1827                        match = INLINE_EXP.match(line)
1828                        if match:
1829                                keyword, value = match.groups()
1830                                self.properties[keyword] = value.strip()
[408]1831
1832                                if self.parameters.debug:
1833                                        self.logger.debug('inline properties: %s : %s' %(keyword,value))
[309]1834                        else:
1835                                body.append(line)
1836                               
1837                return '\n'.join(body)
1838
1839
[154]1840        def wrap_text(self, text, replace_whitespace = False):
[151]1841                """
[191]1842                Will break a lines longer then given length into several small
1843                lines of size given length
[151]1844                """
1845                import textwrap
[154]1846
[151]1847                LINESEPARATOR = '\n'
[153]1848                reformat = ''
[151]1849
[154]1850                for s in text.split(LINESEPARATOR):
1851                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
1852                        if tmp:
1853                                reformat = '%s\n%s' %(reformat,tmp)
1854                        else:
1855                                reformat = '%s\n' %reformat
[153]1856
1857                return reformat
1858
[154]1859                # Python2.4 and higher
1860                #
1861                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1862                #
1863
[343]1864########## EMAIL attachements functions ###########################################################
1865
[340]1866        def inline_part(self, part):
1867                """
1868                """
[406]1869                self.logger.info('function inline_part()')
[154]1870
[340]1871                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1872
[236]1873        def get_message_parts(self, msg):
[45]1874                """
[236]1875                parses the email message and returns a list of body parts and attachments
1876                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
[45]1877                """
[406]1878                self.logger.info('function get_message_parts()')
[317]1879
[309]1880                message_parts = list()
[294]1881       
[278]1882                ALTERNATIVE_MULTIPART = False
1883
[22]1884                for part in msg.walk():
[408]1885                        if self.parameters.debug:
1886                                self.logger.debug('Message part: Main-Type: %s' % part.get_content_maintype())
1887                                self.logger.debug('Message part: Content-Type: %s' % part.get_content_type())
[278]1888
1889                        ## Check content type
[294]1890                        #
1891                        if part.get_content_type() in self.STRIP_CONTENT_TYPES:
[238]1892
[408]1893                                if self.parameters.debug:
1894                                        self.logger.debug("A %s attachment named '%s' was skipped" %(part.get_content_type(), part.get_filename()))
[238]1895
1896                                continue
1897
[294]1898                        ## Catch some mulitpart execptions
1899                        #
1900                        if part.get_content_type() == 'multipart/alternative':
[278]1901                                ALTERNATIVE_MULTIPART = True
1902                                continue
1903
[294]1904                        ## Skip multipart containers
[278]1905                        #
[45]1906                        if part.get_content_maintype() == 'multipart':
[408]1907                                if self.parameters.debug:
1908                                        self.logger.debug("Skipping multipart container")
[22]1909                                continue
[278]1910                       
[294]1911                        ## 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"
1912                        #
[236]1913                        inline = self.inline_part(part)
1914
[294]1915                        ## Drop HTML message
1916                        #
[278]1917                        if ALTERNATIVE_MULTIPART and self.DROP_ALTERNATIVE_HTML_VERSION:
1918                                if part.get_content_type() == 'text/html':
[408]1919                                        if self.parameters.debug:
1920                                                self.logger.debug('Skipping alternative HTML message')
[278]1921
1922                                        ALTERNATIVE_MULTIPART = False
1923                                        continue
1924
[294]1925                        ## Inline text parts are where the body is
1926                        #
[236]1927                        if part.get_content_type() == 'text/plain' and inline:
[408]1928                                if self.parameters.debug:
1929                                        self.logger.debug('               Inline body part')
[236]1930
[45]1931                                # Try to decode, if fails then do not decode
1932                                #
[90]1933                                body_text = part.get_payload(decode=1)
[45]1934                                if not body_text:                       
[90]1935                                        body_text = part.get_payload(decode=0)
[231]1936
[232]1937                                format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1938                                delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
[231]1939
1940                                if self.REFLOW and not self.VERBATIM_FORMAT and format == 'flowed':
1941                                        body_text = self.reflow(body_text, delsp == 'yes')
[154]1942       
[136]1943                                if self.STRIP_SIGNATURE:
1944                                        body_text = self.strip_signature(body_text)
[22]1945
[191]1946                                if self.STRIP_QUOTES:
1947                                        body_text = self.strip_quotes(body_text)
1948
[309]1949                                if self.INLINE_PROPERTIES:
1950                                        body_text = self.inline_properties(body_text)
1951
[148]1952                                if self.USE_TEXTWRAP:
[151]1953                                        body_text = self.wrap_text(body_text)
[148]1954
[294]1955                                ## Get contents charset (iso-8859-15 if not defined in mail headers)
[45]1956                                #
[100]1957                                charset = part.get_content_charset()
[102]1958                                if not charset:
1959                                        charset = 'iso-8859-15'
1960
[89]1961                                try:
[96]1962                                        ubody_text = unicode(body_text, charset)
[100]1963
1964                                except UnicodeError, detail:
[96]1965                                        ubody_text = unicode(body_text, 'iso-8859-15')
[89]1966
[100]1967                                except LookupError, detail:
[139]1968                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
[100]1969
[236]1970                                if self.VERBATIM_FORMAT:
1971                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1972                                else:
1973                                        message_parts.append('%s' %ubody_text)
1974                        else:
[408]1975                                if self.parameters.debug:
[379]1976                                        s = 'TD:               Filename: %s' % part.get_filename()
1977                                        self.print_unicode(s)
[22]1978
[383]1979                                ##
1980                                #  First try to use email header function to convert filename.
1981                                #  If this fails the use the plan filename
1982                                try:
1983                                        filename = self.email_to_unicode(part.get_filename())
1984                                except UnicodeEncodeError, detail:
1985                                        filename = part.get_filename()
1986
[317]1987                                message_parts.append((filename, part))
[236]1988
1989                return message_parts
1990               
[253]1991        def unique_attachment_names(self, message_parts):
[296]1992                """
1993                """
[236]1994                renamed_parts = []
1995                attachment_names = set()
[296]1996
[331]1997                for item in message_parts:
[236]1998                       
[296]1999                        ## If not an attachment, leave it alone
2000                        #
[331]2001                        if not isinstance(item, tuple):
2002                                renamed_parts.append(item)
[236]2003                                continue
2004                               
[331]2005                        (filename, part) = item
[295]2006
[296]2007                        ## If no filename, use a default one
2008                        #
2009                        if not filename:
[236]2010                                filename = 'untitled-part'
[22]2011
[242]2012                                # Guess the extension from the content type, use non strict mode
2013                                # some additional non-standard but commonly used MIME types
2014                                # are also recognized
2015                                #
2016                                ext = mimetypes.guess_extension(part.get_content_type(), False)
[236]2017                                if not ext:
2018                                        ext = '.bin'
[22]2019
[236]2020                                filename = '%s%s' % (filename, ext)
[22]2021
[348]2022                        ## Discard relative paths for windows/unix in attachment names
[296]2023                        #
[348]2024                        #filename = filename.replace('\\', '/').replace(':', '/')
2025                        filename = filename.replace('\\', '_')
2026                        filename = filename.replace('/', '_')
[347]2027
[296]2028                        #
[236]2029                        # We try to normalize the filename to utf-8 NFC if we can.
2030                        # Files uploaded from OS X might be in NFD.
2031                        # Check python version and then try it
2032                        #
[348]2033                        #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
2034                        #       try:
2035                        #               filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
2036                        #       except TypeError:
2037                        #               pass
[100]2038
[236]2039                        # Make the filename unique for this ticket
2040                        num = 0
2041                        unique_filename = filename
[296]2042                        dummy_filename, ext = os.path.splitext(filename)
[134]2043
[339]2044                        while (unique_filename in attachment_names) or self.attachment_exists(unique_filename):
[236]2045                                num += 1
[296]2046                                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
[236]2047                               
[408]2048                        if self.parameters.debug:
[379]2049                                s = 'TD: Attachment with filename %s will be saved as %s' % (filename, unique_filename)
2050                                self.print_unicode(s)
[100]2051
[236]2052                        attachment_names.add(unique_filename)
2053
2054                        renamed_parts.append((filename, unique_filename, part))
[296]2055       
[236]2056                return renamed_parts
2057                       
2058                       
[253]2059        def attachment_exists(self, filename):
[250]2060
[408]2061                if self.parameters.debug:
[379]2062                        s = 'TD: attachment already exists: Id : %s, Filename : %s' %(self.id, filename)
2063                        self.print_unicode(s)
[250]2064
2065                # We have no valid ticket id
2066                #
[253]2067                if not self.id:
[236]2068                        return False
[250]2069
[236]2070                try:
[359]2071                        if self.system == 'discussion':
2072                                att = attachment.Attachment(self.env, 'discussion', 'ticket/%s'
2073                                  % (self.id,), filename)
2074                        else:
2075                                att = attachment.Attachment(self.env, 'ticket', self.id,
2076                                  filename)
[236]2077                        return True
[250]2078                except attachment.ResourceNotFound:
[236]2079                        return False
[343]2080
2081########## TRAC Ticket Text ###########################################################
[236]2082                       
2083        def body_text(self, message_parts):
2084                body_text = []
2085               
2086                for part in message_parts:
2087                        # Plain text part, append it
2088                        if not isinstance(part, tuple):
2089                                body_text.extend(part.strip().splitlines())
2090                                body_text.append("")
2091                                continue
2092                               
2093                        (original, filename, part) = part
2094                        inline = self.inline_part(part)
2095                       
2096                        if part.get_content_maintype() == 'image' and inline:
[359]2097                                if self.system != 'discussion':
2098                                        body_text.append('[[Image(%s)]]' % filename)
[236]2099                                body_text.append("")
2100                        else:
[359]2101                                if self.system != 'discussion':
2102                                        body_text.append('[attachment:"%s"]' % filename)
[236]2103                                body_text.append("")
2104                               
2105                body_text = '\r\n'.join(body_text)
2106                return body_text
2107
[343]2108        def html_mailto_link(self, subject):
2109                """
2110                This function returns a HTML mailto tag with the ticket id and author email address
2111                """
2112                if not self.author:
2113                        author = self.email_addr
2114                else:   
2115                        author = self.author
2116
2117                # use urllib to escape the chars
2118                #
2119                s = 'mailto:%s?Subject=%s&Cc=%s' %(
2120                       urllib.quote(self.email_addr),
2121                           urllib.quote('Re: #%s: %s' %(self.id, subject)),
2122                           urllib.quote(self.MAILTO_CC)
2123                           )
2124
2125                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)
2126                return s
2127
2128########## TRAC notify section ###########################################################
2129
[253]2130        def notify(self, tkt, new=True, modtime=0):
[79]2131                """
2132                A wrapper for the TRAC notify function. So we can use templates
2133                """
[406]2134                self.logger.info('function notify()')
[392]2135
[407]2136                if self.parameters.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.