source: trunk/email2trac.py.in @ 450

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

Some more variables ported to UserDict?

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