source: trunk/email2trac.py.in @ 421

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

ported self.SPAM_HEADER to new syntax self.parameters.spam_header

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