source: trunk/email2trac.py.in @ 446

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

converted VERBATIM_FORMAT to UserDict?

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