source: trunk/email2trac.py.in @ 447

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

ported DROP_ALTERNATIVE_HTML_VERSION to UserDict?

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