source: trunk/email2trac.py.in @ 417

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

Some more logging imporvements

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