source: trunk/email2trac.py.in @ 470

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

some reformat of code

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 58.3 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 470 2010-08-05 10:19:19Z 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#
87m = None
[22]88
[404]89class SaraDict(UserDict.UserDict):
90        def __init__(self, dictin = None):
91                UserDict.UserDict.__init__(self)
92                self.name = None
93               
94                if dictin:
95                        if dictin.has_key('name'):
96                                self.name = dictin['name']
97                                del dictin['name']
98                        self.data = dictin
99                       
[452]100        def get_value(self, name):
101                if self.has_key(name):
102                        return self[name]
[404]103                else:
104                        return None
105                               
106        def __repr__(self):
107                return repr(self.data)
[182]108
[404]109        def __str__(self):
110                return str(self.data)
111                       
112        def __getattr__(self, name):
113                """
114                override the class attribute get method. Return the value
[409]115                from the dictionary
[404]116                """
117                if self.data.has_key(name):
118                        return self.data[name]
119                else:
120                        return None
121                       
122        def __setattr__(self, name, value):
123                """
124                override the class attribute set method only when the UserDict
125                has set its class attribute
126                """
127                if self.__dict__.has_key('data'):
128                        self.data[name] = value
129                else:
130                        self.__dict__[name] = value
131
132        def __iter__(self):
133                return iter(self.data.keys())
134
[22]135class TicketEmailParser(object):
136        env = None
137        comment = '> '
[359]138
[410]139        def __init__(self, env, parameters, logger, version):
[22]140                self.env = env
141
142                # Database connection
143                #
144                self.db = None
145
[206]146                # Save parameters
147                #
148                self.parameters = parameters
[410]149                self.logger = logger
[206]150
[72]151                # Some useful mail constants
152                #
[287]153                self.email_name = None
[72]154                self.email_addr = None
[183]155                self.email_from = None
[288]156                self.author     = None
[287]157                self.id         = None
[294]158               
159                self.STRIP_CONTENT_TYPES = list()
[72]160
[452]161                ## fields properties via body_text
162                #
163                self.properties = dict()
164
[22]165                self.VERSION = version
[402]166
[404]167                self.get_config = self.env.config.get
168
[410]169                ## init function ##
170                #
[402]171                self.setup_parameters()
172
[404]173        def setup_parameters(self):
[434]174                if self.parameters.umask:
[452]175                        os.umask(self.parameters.umask)
[402]176
[452]177                if not self.parameters.spam_level:
[435]178                        self.parameters.spam_level = 0
[22]179
[421]180                if not self.parameters.spam_header:
181                        self.parameters.spam_header = 'X-Spam-Score'
[207]182
[437]183                if not self.parameters.email_quote:
[435]184                        self.parameters.email_quote = '> '
[22]185
[452]186                if not self.parameters.ticket_update_by_subject_lookback:
[445]187                        self.parameters.ticket_update_by_subject_lookback = 30
[360]188
[448]189                if self.parameters.verbatim_format == None:
[446]190                        self.parameters.verbatim_format = 1
[118]191
[448]192                if self.parameters.reflow == None:
[447]193                        self.parameters.reflow = 1
[231]194
[450]195                if self.parameters.binhex:
[294]196                        self.STRIP_CONTENT_TYPES.append('application/mac-binhex40')
[238]197
[450]198                if self.parameters.applesingle:
[294]199                        self.STRIP_CONTENT_TYPES.append('application/applefile')
[238]200
[450]201                if self.parameters.appledouble:
[294]202                        self.STRIP_CONTENT_TYPES.append('application/applefile')
[238]203
[450]204                if self.parameters.strip_content_types:
205                        items = self.parameters.strip_content_types.split(',')
[294]206                        for item in items:
207                                self.STRIP_CONTENT_TYPES.append(item.strip())
208
[450]209                if self.parameters.tmpdir:
210                        self.parameters.tmpdir = os.path.normcase(str(self.parameters['tmpdir']))
211                else:
212                        self.parameters.tmpdir = os.path.normcase('/tmp')
[257]213
[450]214                if self.parameters.email_triggers_workflow == None:
215                        self.parameters.email_triggers_workflow = 1
[191]216
[451]217                if not self.parameters.subject_field_separator:
218                        self.parameters.subject_field_separator = '&'
[297]219                else:
[451]220                        self.parameters.subject_field_separator = self.parameters.subject_field_separator.strip()
[297]221
[305]222                self.trac_smtp_from = self.get_config('notification', 'smtp_from')
223
[359]224                self.system = None
225
[341]226########## Email Header Functions ###########################################################
[339]227
[22]228        def spam(self, message):
[191]229                """
230                # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
231                # Note if Spam_level then '*' are included
232                """
[194]233                spam = False
[421]234                if message.has_key(self.parameters.spam_header):
235                        spam_l = string.split(message[self.parameters.spam_header])
[22]236
[207]237                        try:
238                                number = spam_l[0].count('*')
239                        except IndexError, detail:
240                                number = 0
241                               
[435]242                        if number >= self.parameters.spam_level:
[194]243                                spam = True
244                               
[191]245                # treat virus mails as spam
246                #
247                elif message.has_key('X-Virus-found'):                 
[194]248                        spam = True
249
250                # How to handle SPAM messages
251                #
[440]252                if self.parameters.drop_spam and spam:
[194]253
[435]254                        self.logger.info('Message is a SPAM. Automatic ticket insertion refused (SPAM level > %d)' %self.parameters.spam_level)
[204]255                        return 'drop'   
[194]256
257                elif spam:
258
[204]259                        return 'Spam'   
[194]260                else:
[22]261
[204]262                        return False
[191]263
[221]264        def email_header_acl(self, keyword, header_field, default):
[206]265                """
[221]266                This function wil check if the email address is allowed or denied
267                to send mail to the ticket list
268            """
[409]269                self.logger.debug('function email_header_acl: %s' %keyword)
270
[206]271                try:
[221]272                        mail_addresses = self.parameters[keyword]
273
274                        # Check if we have an empty string
275                        #
276                        if not mail_addresses:
277                                return default
278
[206]279                except KeyError, detail:
[416]280                        self.logger.debug('%s not defined, all messages are allowed.' %(keyword))
[206]281
[221]282                        return default
[206]283
[221]284                mail_addresses = string.split(mail_addresses, ',')
285
286                for entry in mail_addresses:
[209]287                        entry = entry.strip()
[221]288                        TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
289                        result =  TO_RE.search(header_field)
[208]290                        if result:
291                                return True
[149]292
[208]293                return False
294
[22]295        def email_header_txt(self, m):
[72]296                """
297                Display To and CC addresses in description field
298                """
[288]299                s = ''
[334]300
[213]301                if m['To'] and len(m['To']) > 0:
[288]302                        s = "'''To:''' %s\r\n" %(m['To'])
[22]303                if m['Cc'] and len(m['Cc']) > 0:
[288]304                        s = "%s'''Cc:''' %s\r\n" % (s, m['Cc'])
[22]305
[288]306                return  self.email_to_unicode(s)
[22]307
[138]308
[194]309        def get_sender_info(self, message):
[45]310                """
[72]311                Get the default author name and email address from the message
[226]312                """
[43]313
[226]314                self.email_to = self.email_to_unicode(message['to'])
315                self.to_name, self.to_email_addr = email.Utils.parseaddr (self.email_to)
316
[194]317                self.email_from = self.email_to_unicode(message['from'])
[287]318                self.email_name, self.email_addr  = email.Utils.parseaddr(self.email_from)
[142]319
[304]320                ## Trac can not handle author's name that contains spaces
321                #  and forbid the ticket email address as author field
[194]322
[305]323                if self.email_addr == self.trac_smtp_from:
[395]324                        if self.email_name:
325                                self.author = self.email_name
326                        else:
327                                self.author = "email2trac"
[304]328                else:
329                        self.author = self.email_addr
330
[450]331                if self.parameters.ignore_trac_user_settings:
[194]332                        return
333
334                # Is this a registered user, use email address as search key:
335                # result:
336                #   u : login name
337                #   n : Name that the user has set in the settings tab
338                #   e : email address that the user has set in the settings tab
[45]339                #
[194]340                users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
[250]341                        if e and (e.lower() == self.email_addr.lower()) ]
[43]342
[45]343                if len(users) == 1:
[194]344                        self.email_from = users[0][0]
[250]345                        self.author = users[0][0]
[45]346
[72]347        def set_reply_fields(self, ticket, message):
348                """
349                Set all the right fields for a new ticket
350                """
[409]351                self.logger.debug('function set_reply_fields')
[72]352
[270]353                ## Only use name or email adress
354                #ticket['reporter'] = self.email_from
355                ticket['reporter'] = self.author
356
357
[45]358                # Put all CC-addresses in ticket CC field
[43]359                #
[439]360                if self.parameters.reply_all:
[43]361
[299]362                        email_cc = ''
363
364                        cc_addrs = email.Utils.getaddresses( message.get_all('cc', []) )
365
366                        if not cc_addrs:
[105]367                                return
[43]368
[299]369                        ## Build a list of forbidden CC addresses
370                        #
[300]371                        #to_addrs = email.Utils.getaddresses( message.get_all('to', []) )
372                        #to_list = list()
373                        #for n,e in to_addrs:
374                        #       to_list.append(e)
[299]375                               
[392]376                        # Always Remove reporter email address from cc-list
[43]377                        #
[392]378                        try:
379                                cc_addrs.remove((self.author, self.email_addr))
380                        except ValueError, detail:
381                                pass
[43]382
[299]383                        for name,addr in cc_addrs:
384               
385                                ## Prevent mail loop
386                                #
[300]387                                #if addr in to_list:
[304]388
389                                if addr == self.trac_smtp_from:
[416]390                                        self.logger.debug("Skipping %s mail address for CC-field" %(addr))
[299]391                                        continue
[43]392
[299]393                                if email_cc:
394                                        email_cc = '%s, %s' %(email_cc, addr)
395                                else:
396                                        email_cc = addr
[96]397
[299]398                        if email_cc:
[416]399                                self.logger.debug('set_reply_fields: %s' %email_cc)
[299]400
401                                ticket['cc'] = self.email_to_unicode(email_cc)
402
[339]403
404########## DEBUG functions  ###########################################################
405
[310]406        def debug_body(self, message_body, tempfile=False):
407                if tempfile:
408                        import tempfile
409                        body_file = tempfile.mktemp('.email2trac')
410                else:
[450]411                        body_file = os.path.join(self.parameters.tmpdir, 'body.txt')
[310]412
[407]413                if self.parameters.dry_run:
[331]414                        print 'DRY-RUN: not saving body to %s' %(body_file)
415                        return
416
[433]417                print 'writing body to %s' %(body_file)
[331]418                fx = open(body_file, 'wb')
[310]419                if not message_body:
[331]420                                message_body = '(None)'
[310]421
422                message_body = message_body.encode('utf-8')
423                #message_body = unicode(message_body, 'iso-8859-15')
424
425                fx.write(message_body)
426                fx.close()
427                try:
428                        os.chmod(body_file,S_IRWXU|S_IRWXG|S_IRWXO)
429                except OSError:
430                        pass
431
432        def debug_attachments(self, message_parts):
[317]433                """
434                """
[409]435                self.logger.debug('function debug_attachments')
[317]436               
[310]437                n = 0
[331]438                for item in message_parts:
[310]439                        # Skip inline text parts
[331]440                        if not isinstance(item, tuple):
[310]441                                continue
442                               
[331]443                        (original, filename, part) = item
[310]444
445                        n = n + 1
[433]446                        print 'part%d: Content-Type: %s' % (n, part.get_content_type())
[379]447               
[433]448                        s = 'part%d: filename: %s' %(n, filename)
[379]449                        self.print_unicode(s)
450       
[348]451                        ## Forbidden chars
452                        #
453                        filename = filename.replace('\\', '_')
454                        filename = filename.replace('/', '_')
455       
456
[450]457                        part_file = os.path.join(self.parameters.tmpdir, filename)
[433]458                        s = 'writing part%d (%s)' % (n,part_file)
[379]459                        self.print_unicode(s)
[331]460
[407]461                        if self.parameters.dry_run:
[331]462                                print 'DRY_RUN: NOT saving attachments'
463                                continue
464
[377]465                        part_file = util.text.unicode_quote(part_file)
466
[310]467                        fx = open(part_file, 'wb')
468                        text = part.get_payload(decode=1)
[331]469
[310]470                        if not text:
471                                text = '(None)'
[331]472
[310]473                        fx.write(text)
474                        fx.close()
[331]475
[310]476                        try:
477                                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
478                        except OSError:
479                                pass
480
[466]481        def save_email_for_debug(self, message, create_tempfile=False):
[309]482
[466]483                if create_tempfile:
[96]484                        import tempfile
485                        msg_file = tempfile.mktemp('.email2trac')
486                else:
[173]487                        #msg_file = '/var/tmp/msg.txt'
[450]488                        msg_file = os.path.join(self.parameters.tmpdir, 'msg.txt')
[173]489
[407]490                if self.parameters.dry_run:
[331]491                        print 'DRY_RUN: NOT saving email message to %s' %(msg_file)
492                else:
[433]493                        print 'saving email to %s' %(msg_file)
[44]494
[331]495                        fx = open(msg_file, 'wb')
496                        fx.write('%s' % message)
497                        fx.close()
498                       
499                        try:
500                                os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
501                        except OSError:
502                                pass
503
[309]504                message_parts = self.get_message_parts(message)
505                message_parts = self.unique_attachment_names(message_parts)
506                body_text = self.body_text(message_parts)
507                self.debug_body(body_text, True)
508                self.debug_attachments(message_parts)
509
[339]510########## Conversion functions  ###########################################################
511
[341]512        def email_to_unicode(self, message_str):
513                """
514                Email has 7 bit ASCII code, convert it to unicode with the charset
515                that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
516                understands it.
517                """
[409]518                self.logger.debug("function email_to_unicode")
[341]519
520                results =  email.Header.decode_header(message_str)
521
522                s = None
523                for text,format in results:
524                        if format:
525                                try:
526                                        temp = unicode(text, format)
527                                except UnicodeError, detail:
528                                        # This always works
529                                        #
530                                        temp = unicode(text, 'iso-8859-15')
531                                except LookupError, detail:
532                                        #text = 'ERROR: Could not find charset: %s, please install' %format
533                                        #temp = unicode(text, 'iso-8859-15')
534                                        temp = message_str
535                                       
536                        else:
537                                temp = string.strip(text)
538                                temp = unicode(text, 'iso-8859-15')
539
540                        if s:
541                                s = '%s %s' %(s, temp)
542                        else:
543                                s = '%s' %temp
544
545                #s = s.encode('utf-8')
546                return s
547
[288]548        def str_to_dict(self, s):
[164]549                """
[288]550                Transfrom a string of the form [<key>=<value>]+ to dict[<key>] = <value>
[164]551                """
[409]552                self.logger.debug("function str_to_dict")
[164]553
[451]554                fields = string.split(s, self.parameters.subject_field_separator)
[262]555
[164]556                result = dict()
557                for field in fields:
558                        try:
[262]559                                index, value = string.split(field, '=')
[169]560
561                                # We can not change the description of a ticket via the subject
562                                # line. The description is the body of the email
563                                #
564                                if index.lower() in ['description']:
565                                        continue
566
[164]567                                if value:
[165]568                                        result[index.lower()] = value
[169]569
[164]570                        except ValueError:
571                                pass
[165]572                return result
[167]573
[379]574        def print_unicode(self,s):
575                """
576                This function prints unicode strings uif possible else it will quote it
577                """
578                try:
[408]579                        self.logger.debug(s)
[379]580                except UnicodeEncodeError, detail:
[408]581                        self.logger.debug(util.text.unicode_quote(s))
[379]582
[339]583########## TRAC ticket functions  ###########################################################
584
[398]585        def check_permission_participants(self, tkt):
[397]586                """
[398]587                Check if the mailer is allowed to update the ticket
588                """
[409]589                self.logger.debug('function check_permission_participants')
[398]590
591                if tkt['reporter'].lower() in [self.author, self.email_addr]:
[416]592                        self.logger.debug('ALLOW, %s is the ticket reporter' %(self.email_addr))
[408]593
[398]594                        return True
595
596                perm = PermissionSystem(self.env)
597                if perm.check_permission('TICKET_MODIFY', self.author):
[416]598                        self.logger.debug('ALLOW, %s has trac permission to update the ticket' %(self.author))
[408]599
[398]600                        return True
[408]601
[398]602                else:
603                        return False
604               
605
606                # Is the updater in the CC?
607                try:
608                        cc_list = tkt['cc'].split(',')
609                        for cc in cc_list:
610                                if self.email_addr.lower() in cc.strip():
[416]611                                        self.logger.debug('ALLOW, %s is in the CC' %(self.email_addr))
[398]612
613                                        return True
614
615                except KeyError:
616                        return False
617
618        def check_permission(self, tkt, action):
619                """
[397]620                check if the reporter has the right permission for the action:
621          - TICKET_CREATE
622          - TICKET_MODIFY
623
624                There are three models:
625                        - None      : no checking at all
626                        - trac      : check the permission via trac permission model
627                        - email2trac: ....
628                """
[409]629                self.logger.debug("function check_permission")
[397]630
[441]631                if self.parameters.ticket_permission_system in ['trac']:
[398]632
[397]633                        perm = PermissionSystem(self.env)
634                        if perm.check_permission(action, self.author):
635                                return True
636                        else:
637                                return False
[398]638
[441]639                elif self.parameters.ticket_permission_system in ['update_restricted_to_participants']:
[398]640                        if action in ['TICKET_MODIFY']:
641                                return (self.check_permission_participants(tkt))       
642                        else:
643                                return True
644
645                # Default is to allow everybody ticket updates and ticket creation
[397]646                else:
647                                return True
648
649
[202]650        def update_ticket_fields(self, ticket, user_dict, use_default=None):
651                """
652                This will update the ticket fields. It will check if the
653                given fields are known and if the right values are specified
654                It will only update the ticket field value:
[169]655                        - If the field is known
[202]656                        - If the value supplied is valid for the ticket field.
657                          If not then there are two options:
658                           1) Skip the value (use_default=None)
659                           2) Set default value for field (use_default=1)
[169]660                """
[409]661                self.logger.debug("function update_ticket_fields")
[169]662
663                # Build a system dictionary from the ticket fields
664                # with field as index and option as value
665                #
666                sys_dict = dict()
667                for field in ticket.fields:
[167]668                        try:
[169]669                                sys_dict[field['name']] = field['options']
670
[167]671                        except KeyError:
[169]672                                sys_dict[field['name']] = None
[167]673                                pass
[169]674
[301]675                ## Check user supplied fields an compare them with the
[169]676                # system one's
677                #
678                for field,value in user_dict.items():
[408]679                        if self.parameters.debug:
[433]680                                s = 'user_field\t %s = %s' %(field,value)
[379]681                                self.print_unicode(s)
[169]682
[301]683                        ## To prevent mail loop
684                        #
685                        if field == 'cc':
686
687                                cc_list = user_dict['cc'].split(',')
688
[304]689                                if self.trac_smtp_from in cc_list:
[433]690                                        self.logger.debug('MAIL LOOP: %s is not allowed as CC address' %(self.trac_smtp_from))
[408]691
[304]692                                        cc_list.remove(self.trac_smtp_from)
[301]693
694                                value = ','.join(cc_list)
695                               
696
[169]697                        if sys_dict.has_key(field):
698
699                                # Check if value is an allowed system option, if TypeError then
700                                # every value is allowed
701                                #
702                                try:
703                                        if value in sys_dict[field]:
704                                                ticket[field] = value
[202]705                                        else:
706                                                # Must we set a default if value is not allowed
707                                                #
708                                                if use_default:
709                                                        value = self.get_config('ticket', 'default_%s' %(field) )
[169]710
711                                except TypeError:
[345]712                                        pass
713
714                                ## Only set if we have a value
715                                #
716                                if value:
[169]717                                        ticket[field] = value
[202]718
[408]719                                if self.parameters.debug:
[379]720                                        s = 'ticket_field\t %s = %s' %(field,  ticket[field])
721                                        self.print_unicode(s)
[345]722
[260]723        def ticket_update(self, m, id, spam):
[78]724                """
[79]725                If the current email is a reply to an existing ticket, this function
726                will append the contents of this email to that ticket, instead of
727                creating a new one.
[78]728                """
[409]729                self.logger.debug("function ticket_update")
[202]730
[456]731                if not self.parameters.ticket_update:
732                        self.logger.debug("ticket_update disabled")
733                        return False
734
[164]735                # Must we update ticket fields
736                #
[220]737                update_fields = dict()
[165]738                try:
[260]739                        id, keywords = string.split(id, '?')
[262]740
[220]741                        update_fields = self.str_to_dict(keywords)
[165]742
743                        # Strip '#'
744                        #
[260]745                        self.id = int(id[1:])
[165]746
[260]747                except ValueError:
[390]748
[362]749                        # Strip '#'
[165]750                        #
[362]751                        self.id = int(id[1:])
[164]752
[456]753                self.logger.debug("ticket_update id %s" %id)
[71]754
[194]755                # When is the change committed
756                #
[386]757                if self.VERSION < 0.11:
758                        when = int(time.time())
759                else:
[431]760                        when = datetime.now(util.datefmt.utc)
[77]761
[172]762                try:
[253]763                        tkt = Ticket(self.env, self.id, self.db)
[390]764
[172]765                except util.TracError, detail:
[390]766
[253]767                        # Not a valid ticket
[390]768
[253]769                        self.id = None
[172]770                        return False
[126]771
[398]772                # Check the permission of the reporter
773                #
[441]774                if self.parameters.ticket_permission_system:
[398]775                        if not self.check_permission(tkt, 'TICKET_MODIFY'):
[409]776                                self.logger.info('Reporter: %s has no permission to modify tickets' %self.author)
[398]777                                return False
778
[468]779                ## How many changes has this ticket
780                #
[288]781                cnum = len(tkt.get_changelog())
782
[468]783                ## reopen the ticket if it is was closed
784                #  We must use the ticket workflow framework
[220]785                #
[469]786                if self.parameters.email_triggers_workflow and (self.VERSION >= 0.11):
[220]787
[469]788                        self.logger.debug('Workflow ticket update fields: ')
[257]789
[469]790                        from trac.ticket.default_workflow import ConfigurableTicketWorkflow
791                        from trac.test import Mock, MockPerm
[468]792
[469]793                        req = Mock(authname=self.author, perm=MockPerm(), args={})
794                        try:
[468]795                                workflow = self.parameters['workflow_%s' %tkt['status']]
[469]796                        except KeyError:
[468]797                                ## fallback for compability (Will be deprecated)
798                                #
[469]799                                if tkt['status'] in ['closed']:
800                                        workflow = self.parameters.workflow
801                                else:   
802                                        workflow = None
[468]803
[469]804                        controller = ConfigurableTicketWorkflow(self.env)
805                        #print controller.actions
806                        #print controller.actions.keys()
807                        #print controller.get_ticket_actions(req, tkt)
808                        #print controller.actions[workflow]
809                        #print controller.actions[workflow]['permissions'] is a list
810
[468]811                        if workflow:
[257]812
[469]813                                if self.parameters.ticket_permission_system:
[257]814
[469]815                                        if self.check_permission(tkt, controller.actions[workflow]['permissions'][0]):
816                                                fields = controller.get_ticket_changes(req, tkt, workflow)
817                                        else:
818                                                fields = dict()
819                                                self.logger.info('Reporter: %s has no permission to trigger workflow' %self.author)
[257]820
[469]821                                else:
822                                        fields = controller.get_ticket_changes(req, tkt, workflow)
[257]823
824                                for key in fields.keys():
[416]825                                        self.logger.debug('\t %s : %s' %(key, fields[key]))
[257]826                                        tkt[key] = fields[key]
827
[469]828                ## Old pre 0.11 situation
829                #
830                elif self.parameters.email_triggers_workflow:
[470]831
[469]832                        self.logger.debug('email triggers workflow pre trac 0.11')
[470]833
[469]834                        if tkt['status'] in ['closed']:
[257]835                                tkt['status'] = 'reopened'
836                                tkt['resolution'] = ''
837
[469]838                else:
839                        self.logger.debug('email triggers workflow disabled')
840
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)]
[467]912                                if self.parameters.debug:
[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                        #
[456]1509                        #if result.group('reply') and self.parameters.ticket_update:
1510                        if result.group('reply'):
[359]1511                                self.system = 'ticket'
[260]1512
[390]1513                                # Skip the last ':' character
1514                                #
1515                                if not self.ticket_update(m, result.group('reply')[:-1], spam_msg):
1516                                        self.new_ticket(m, subject, spam_msg)
1517
[262]1518                        # New ticket + fields
1519                        #
1520                        elif result.group('new_fields'):
[359]1521                                self.system = 'ticket'
[262]1522                                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
1523
[359]1524                        if blog_enabled:
1525                                if result.group('blog'):
1526                                        self.system = 'blog'
1527                                        self.blog(result.group('blog_id'))
1528
1529                        if discussion_enabled:
1530                                # New topic.
1531                                #
1532                                if result.group('forum'):
1533                                        self.system = 'discussion'
1534                                        self.id = int(result.group('forum_id'))
1535                                        self.discussion_topic(m, subject[result.end('forum'):])
1536
1537                                # Reply to topic.
1538                                #
1539                                elif result.group('topic'):
1540                                        self.system = 'discussion'
1541                                        self.id = int(result.group('topic_id'))
1542                                        self.discussion_topic_reply(m, subject[result.end('topic'):])
1543
1544                                # Reply to topic message.
1545                                #
1546                                elif result.group('message'):
1547                                        self.system = 'discussion'
1548                                        self.id = int(result.group('message_id'))
1549                                        self.discussion_message_reply(m, subject[result.end('message'):])
1550
[260]1551                else:
[359]1552                        self.system = 'ticket'
[356]1553                        result = self.ticket_update_by_subject(subject)
1554                        if result:
[390]1555                                if not self.ticket_update(m, result, spam_msg):
1556                                        self.new_ticket(m, subject, spam_msg)
[353]1557                        else:
1558                                # No update by subject, so just create a new ticket
1559                                self.new_ticket(m, subject, spam_msg)
1560
[356]1561
[343]1562########## BODY TEXT functions  ###########################################################
1563
[136]1564        def strip_signature(self, text):
1565                """
1566                Strip signature from message, inspired by Mailman software
1567                """
[462]1568                self.logger.debug('function strip_signature')
1569
[136]1570                body = []
1571                for line in text.splitlines():
1572                        if line == '-- ':
1573                                break
1574                        body.append(line)
1575
1576                return ('\n'.join(body))
1577
[231]1578        def reflow(self, text, delsp = 0):
1579                """
1580                Reflow the message based on the format="flowed" specification (RFC 3676)
1581                """
1582                flowedlines = []
1583                quotelevel = 0
1584                prevflowed = 0
1585
1586                for line in text.splitlines():
1587                        from re import match
1588                       
1589                        # Figure out the quote level and the content of the current line
1590                        m = match('(>*)( ?)(.*)', line)
1591                        linequotelevel = len(m.group(1))
1592                        line = m.group(3)
1593
1594                        # Determine whether this line is flowed
1595                        if line and line != '-- ' and line[-1] == ' ':
1596                                flowed = 1
1597                        else:
1598                                flowed = 0
1599
1600                        if flowed and delsp and line and line[-1] == ' ':
1601                                line = line[:-1]
1602
1603                        # If the previous line is flowed, append this line to it
1604                        if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1605                                flowedlines[-1] += line
1606                        # Otherwise, start a new line
1607                        else:
1608                                flowedlines.append('>' * linequotelevel + line)
1609
1610                        prevflowed = flowed
1611                       
1612
1613                return '\n'.join(flowedlines)
1614
[191]1615        def strip_quotes(self, text):
[193]1616                """
1617                Strip quotes from message by Nicolas Mendoza
1618                """
[462]1619                self.logger.debug('function strip_quotes')
1620
[193]1621                body = []
1622                for line in text.splitlines():
[462]1623                        try:
1624
1625                                if line.startswith(self.parameters.email_quote):
1626                                        continue
1627
1628                        except UnicodeDecodeError:
1629
1630                                tmp_line = self.email_to_unicode(line)
1631                                if tmp_line.startswith(self.parameters.email_quote):
1632                                        continue
1633                               
[193]1634                        body.append(line)
[151]1635
[193]1636                return ('\n'.join(body))
[191]1637
[309]1638        def inline_properties(self, text):
1639                """
1640                Parse text if we use inline keywords to set ticket fields
1641                """
[409]1642                self.logger.debug('function inline_properties')
[309]1643
1644                properties = dict()
1645                body = list()
1646
1647                INLINE_EXP = re.compile('\s*[@]\s*([a-zA-Z]+)\s*:(.*)$')
1648
1649                for line in text.splitlines():
1650                        match = INLINE_EXP.match(line)
1651                        if match:
1652                                keyword, value = match.groups()
1653                                self.properties[keyword] = value.strip()
[408]1654
[416]1655                                self.logger.debug('inline properties: %s : %s' %(keyword,value))
1656
[309]1657                        else:
1658                                body.append(line)
1659                               
1660                return '\n'.join(body)
1661
1662
[154]1663        def wrap_text(self, text, replace_whitespace = False):
[151]1664                """
[191]1665                Will break a lines longer then given length into several small
1666                lines of size given length
[151]1667                """
1668                import textwrap
[154]1669
[151]1670                LINESEPARATOR = '\n'
[153]1671                reformat = ''
[151]1672
[154]1673                for s in text.split(LINESEPARATOR):
[449]1674                        tmp = textwrap.fill(s, self.parameters.use_textwrap)
[154]1675                        if tmp:
1676                                reformat = '%s\n%s' %(reformat,tmp)
1677                        else:
1678                                reformat = '%s\n' %reformat
[153]1679
1680                return reformat
1681
[154]1682                # Python2.4 and higher
1683                #
1684                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1685                #
1686
[343]1687########## EMAIL attachements functions ###########################################################
1688
[340]1689        def inline_part(self, part):
1690                """
1691                """
[409]1692                self.logger.debug('function inline_part()')
[154]1693
[340]1694                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1695
[236]1696        def get_message_parts(self, msg):
[45]1697                """
[236]1698                parses the email message and returns a list of body parts and attachments
1699                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
[45]1700                """
[409]1701                self.logger.debug('function get_message_parts()')
[317]1702
[309]1703                message_parts = list()
[294]1704       
[278]1705                ALTERNATIVE_MULTIPART = False
1706
[22]1707                for part in msg.walk():
[416]1708                        self.logger.debug('Message part: Main-Type: %s' % part.get_content_maintype())
1709                        self.logger.debug('Message part: Content-Type: %s' % part.get_content_type())
[278]1710
1711                        ## Check content type
[294]1712                        #
1713                        if part.get_content_type() in self.STRIP_CONTENT_TYPES:
[416]1714                                self.logger.debug("A %s attachment named '%s' was skipped" %(part.get_content_type(), part.get_filename()))
[238]1715                                continue
1716
[294]1717                        ## Catch some mulitpart execptions
1718                        #
1719                        if part.get_content_type() == 'multipart/alternative':
[278]1720                                ALTERNATIVE_MULTIPART = True
1721                                continue
1722
[294]1723                        ## Skip multipart containers
[278]1724                        #
[45]1725                        if part.get_content_maintype() == 'multipart':
[416]1726                                self.logger.debug("Skipping multipart container")
1727
[22]1728                                continue
[278]1729                       
[294]1730                        ## Check if this is an inline part. It's inline if there is co Cont-Disp header, or if there is one and it says "inline"
1731                        #
[236]1732                        inline = self.inline_part(part)
1733
[294]1734                        ## Drop HTML message
1735                        #
[447]1736                        if ALTERNATIVE_MULTIPART and self.parameters.drop_alternative_html_version:
[278]1737                                if part.get_content_type() == 'text/html':
[416]1738                                        self.logger.debug('Skipping alternative HTML message')
1739                                        ALTERNATIVE_MULTIPART = False
[278]1740                                        continue
1741
[294]1742                        ## Inline text parts are where the body is
1743                        #
[236]1744                        if part.get_content_type() == 'text/plain' and inline:
[416]1745                                self.logger.debug('               Inline body part')
[236]1746
[45]1747                                # Try to decode, if fails then do not decode
1748                                #
[90]1749                                body_text = part.get_payload(decode=1)
[45]1750                                if not body_text:                       
[90]1751                                        body_text = part.get_payload(decode=0)
[231]1752
[232]1753                                format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1754                                delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
[231]1755
[447]1756                                if self.parameters.reflow and not self.parameters.verbatim_format and format == 'flowed':
[231]1757                                        body_text = self.reflow(body_text, delsp == 'yes')
[154]1758       
[448]1759                                if self.parameters.strip_signature:
[136]1760                                        body_text = self.strip_signature(body_text)
[22]1761
[448]1762                                if self.parameters.strip_quotes:
[191]1763                                        body_text = self.strip_quotes(body_text)
1764
[449]1765                                if self.parameters.inline_properties:
[309]1766                                        body_text = self.inline_properties(body_text)
1767
[449]1768                                if self.parameters.use_textwrap:
[151]1769                                        body_text = self.wrap_text(body_text)
[148]1770
[294]1771                                ## Get contents charset (iso-8859-15 if not defined in mail headers)
[45]1772                                #
[100]1773                                charset = part.get_content_charset()
[102]1774                                if not charset:
1775                                        charset = 'iso-8859-15'
1776
[89]1777                                try:
[96]1778                                        ubody_text = unicode(body_text, charset)
[100]1779
1780                                except UnicodeError, detail:
[96]1781                                        ubody_text = unicode(body_text, 'iso-8859-15')
[89]1782
[100]1783                                except LookupError, detail:
[139]1784                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
[100]1785
[446]1786                                if self.parameters.verbatim_format:
[236]1787                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1788                                else:
1789                                        message_parts.append('%s' %ubody_text)
1790                        else:
[408]1791                                if self.parameters.debug:
[433]1792                                        s = '              Filename: %s' % part.get_filename()
[379]1793                                        self.print_unicode(s)
[22]1794
[383]1795                                ##
1796                                #  First try to use email header function to convert filename.
1797                                #  If this fails the use the plan filename
1798                                try:
1799                                        filename = self.email_to_unicode(part.get_filename())
1800                                except UnicodeEncodeError, detail:
1801                                        filename = part.get_filename()
1802
[317]1803                                message_parts.append((filename, part))
[236]1804
1805                return message_parts
1806               
[253]1807        def unique_attachment_names(self, message_parts):
[296]1808                """
1809                """
[236]1810                renamed_parts = []
1811                attachment_names = set()
[296]1812
[331]1813                for item in message_parts:
[236]1814                       
[296]1815                        ## If not an attachment, leave it alone
1816                        #
[331]1817                        if not isinstance(item, tuple):
1818                                renamed_parts.append(item)
[236]1819                                continue
1820                               
[331]1821                        (filename, part) = item
[295]1822
[296]1823                        ## If no filename, use a default one
1824                        #
1825                        if not filename:
[236]1826                                filename = 'untitled-part'
[22]1827
[242]1828                                # Guess the extension from the content type, use non strict mode
1829                                # some additional non-standard but commonly used MIME types
1830                                # are also recognized
1831                                #
1832                                ext = mimetypes.guess_extension(part.get_content_type(), False)
[236]1833                                if not ext:
1834                                        ext = '.bin'
[22]1835
[236]1836                                filename = '%s%s' % (filename, ext)
[22]1837
[348]1838                        ## Discard relative paths for windows/unix in attachment names
[296]1839                        #
[348]1840                        #filename = filename.replace('\\', '/').replace(':', '/')
1841                        filename = filename.replace('\\', '_')
1842                        filename = filename.replace('/', '_')
[347]1843
[465]1844                        ## remove linefeed char
[296]1845                        #
[465]1846                        for forbidden_char in ['\r', '\n']:
1847                                filename = filename.replace(forbidden_char,'')
1848
1849                        #
[236]1850                        # We try to normalize the filename to utf-8 NFC if we can.
1851                        # Files uploaded from OS X might be in NFD.
1852                        # Check python version and then try it
1853                        #
[348]1854                        #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
1855                        #       try:
1856                        #               filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
1857                        #       except TypeError:
1858                        #               pass
[100]1859
[236]1860                        # Make the filename unique for this ticket
1861                        num = 0
1862                        unique_filename = filename
[296]1863                        dummy_filename, ext = os.path.splitext(filename)
[134]1864
[339]1865                        while (unique_filename in attachment_names) or self.attachment_exists(unique_filename):
[236]1866                                num += 1
[296]1867                                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
[236]1868                               
[408]1869                        if self.parameters.debug:
[433]1870                                s = 'Attachment with filename %s will be saved as %s' % (filename, unique_filename)
[379]1871                                self.print_unicode(s)
[100]1872
[236]1873                        attachment_names.add(unique_filename)
1874
1875                        renamed_parts.append((filename, unique_filename, part))
[296]1876       
[236]1877                return renamed_parts
1878                       
1879                       
[253]1880        def attachment_exists(self, filename):
[250]1881
[408]1882                if self.parameters.debug:
[433]1883                        s = 'attachment already exists: Id : %s, Filename : %s' %(self.id, filename)
[379]1884                        self.print_unicode(s)
[250]1885
1886                # We have no valid ticket id
1887                #
[253]1888                if not self.id:
[236]1889                        return False
[250]1890
[236]1891                try:
[359]1892                        if self.system == 'discussion':
1893                                att = attachment.Attachment(self.env, 'discussion', 'ticket/%s'
1894                                  % (self.id,), filename)
1895                        else:
1896                                att = attachment.Attachment(self.env, 'ticket', self.id,
1897                                  filename)
[236]1898                        return True
[250]1899                except attachment.ResourceNotFound:
[236]1900                        return False
[343]1901
1902########## TRAC Ticket Text ###########################################################
[236]1903                       
1904        def body_text(self, message_parts):
1905                body_text = []
1906               
1907                for part in message_parts:
1908                        # Plain text part, append it
1909                        if not isinstance(part, tuple):
1910                                body_text.extend(part.strip().splitlines())
1911                                body_text.append("")
1912                                continue
1913                               
1914                        (original, filename, part) = part
1915                        inline = self.inline_part(part)
1916                       
1917                        if part.get_content_maintype() == 'image' and inline:
[359]1918                                if self.system != 'discussion':
1919                                        body_text.append('[[Image(%s)]]' % filename)
[236]1920                                body_text.append("")
1921                        else:
[359]1922                                if self.system != 'discussion':
1923                                        body_text.append('[attachment:"%s"]' % filename)
[236]1924                                body_text.append("")
1925                               
1926                body_text = '\r\n'.join(body_text)
1927                return body_text
1928
[343]1929        def html_mailto_link(self, subject):
1930                """
1931                This function returns a HTML mailto tag with the ticket id and author email address
1932                """
1933                if not self.author:
1934                        author = self.email_addr
1935                else:   
1936                        author = self.author
1937
[434]1938                if not self.parameters.mailto_cc:
1939                        self.parameters.mailto_cc = ''
1940
[343]1941                # use urllib to escape the chars
1942                #
1943                s = 'mailto:%s?Subject=%s&Cc=%s' %(
1944                       urllib.quote(self.email_addr),
1945                           urllib.quote('Re: #%s: %s' %(self.id, subject)),
[434]1946                           urllib.quote(self.parameters.mailto_cc)
[343]1947                           )
1948
1949                s = '\r\n{{{\r\n#!html\r\n<a\r\n href="%s">Reply to: %s\r\n</a>\r\n}}}\r\n' %(s, author)
1950                return s
1951
1952########## TRAC notify section ###########################################################
1953
[253]1954        def notify(self, tkt, new=True, modtime=0):
[79]1955                """
1956                A wrapper for the TRAC notify function. So we can use templates
1957                """
[409]1958                self.logger.debug('function notify()')
[392]1959
[407]1960                if self.parameters.dry_run:
[344]1961                                print 'DRY_RUN: self.notify(tkt, True) reporter = %s' %tkt['reporter']
[250]1962                                return
[41]1963                try:
[392]1964
1965                        #from trac.ticket.web_ui import TicketModule
1966                        #from trac.ticket.notification import TicketNotificationSystem
1967                        #ticket_sys = TicketNotificationSystem(self.env)
1968                        #a = TicketModule(self.env)
1969                        #print a.__dict__
1970                        #tn_sys = TicketNotificationSystem(self.env)
1971                        #print tn_sys
1972                        #print tn_sys.__dict__
1973                        #sys.exit(0)
1974
[41]1975                        # create false {abs_}href properties, to trick Notify()
1976                        #
[369]1977                        if not (self.VERSION in [0.11, 0.12]):
[192]1978                                self.env.abs_href = Href(self.get_config('project', 'url'))
1979                                self.env.href = Href(self.get_config('project', 'url'))
[22]1980
[392]1981
[41]1982                        tn = TicketNotifyEmail(self.env)
[213]1983
[438]1984                        if self.parameters.alternate_notify_template:
[222]1985
[388]1986                                if self.VERSION >= 0.11:
[222]1987
[221]1988                                        from trac.web.chrome import Chrome
[222]1989
[438]1990                                        if  self.parameters.alternate_notify_template_update and not new:
1991                                                tn.template_name = self.parameters.alternate_notify_template_update
[222]1992                                        else:
[438]1993                                                tn.template_name = self.parameters.alternate_notify_template
[222]1994
[221]1995                                        tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
1996                                               
1997                                else:
[222]1998
[438]1999                                        tn.template_name = self.parameters.alternate_notify_template
[42]2000
[77]2001                        tn.notify(tkt, new, modtime)
[41]2002
2003                except Exception, e:
[431]2004                        self.logger.error('Failure sending notification on creation of ticket #%s: %s' %(self.id, e))
[41]2005
[22]2006
[74]2007
[343]2008########## Parse Config File  ###########################################################
[22]2009
2010def ReadConfig(file, name):
2011        """
2012        Parse the config file
2013        """
2014        if not os.path.isfile(file):
[79]2015                print 'File %s does not exist' %file
[22]2016                sys.exit(1)
2017
[199]2018        config = trac_config.Configuration(file)
[22]2019
2020        # Use given project name else use defaults
2021        #
2022        if name:
[199]2023                sections = config.sections()
2024                if not name in sections:
[79]2025                        print "Not a valid project name: %s" %name
[199]2026                        print "Valid names: %s" %sections
[22]2027                        sys.exit(1)
2028
[404]2029                project =  SaraDict()
[199]2030                for option, value in  config.options(name):
[452]2031                        try:
2032                                project[option] = int(value)
2033                        except ValueError:
2034                                project[option] = value
[22]2035
2036        else:
[270]2037                # use some trac internals to get the defaults
[217]2038                #
[404]2039                tmp = config.parser.defaults()
2040                project =  SaraDict()
[22]2041
[452]2042                for option, value in tmp.items():
2043                        try:
2044                                project[option] = int(value)
2045                        except ValueError:
2046                                project[option] = value
[404]2047
[22]2048        return project
2049
[410]2050########## Setup Logging ###############################################################
[87]2051
[419]2052def setup_log(parameters, project_name, interactive=None):
[410]2053        """
2054        Setup loging
2055
[420]2056        Note for log format the usage of `$(...)s` instead of `%(...)s` as the latter form
2057    would be interpreted by the ConfigParser itself.
[410]2058        """
[419]2059        logger = logging.getLogger('email2trac %s' %project_name)
[410]2060
[414]2061        if interactive:
2062                parameters.log_type = 'stderr'
2063
[410]2064        if not parameters.log_type:
2065                parameters.log_type = 'syslog'
2066
2067        if parameters.log_type == 'file':
[415]2068
2069                if not parameters.log_file:
2070                        parameters.log_file = 'email2trac.log'
2071
[410]2072                if not os.path.isabs(parameters.log_file):
[415]2073                        import tempfile
2074                        parameters.log_file = os.path.join(tempfile.gettempdir(), parameters.log_file)
[410]2075
[415]2076                log_handler = logging.FileHandler(parameters.log_file)
2077
[410]2078        elif parameters.log_type in ('winlog', 'eventlog', 'nteventlog'):
2079                # Requires win32 extensions
2080                log_handler = logging.handlers.NTEventLogHandler(logid, logtype='Application')
2081
2082        elif parameters.log_type in ('syslog', 'unix'):
2083                log_handler = logging.handlers.SysLogHandler('/dev/log')
2084
2085        elif parameters.log_type in ('stderr'):
2086                log_handler = logging.StreamHandler(sys.stderr)
2087
2088        else:
2089                log_handler = logging.handlers.BufferingHandler(0)
2090
2091        if parameters.log_format:
2092                parameters.log_format = parameters.log_format.replace('$(', '%(')
2093        else:
[419]2094                parameters.log_format = '%(name)s: %(message)s'
[410]2095
2096        log_formatter = logging.Formatter(parameters.log_format)
2097        log_handler.setFormatter(log_formatter)
2098        logger.addHandler(log_handler)
2099
2100        if (parameters.log_level in ['DEBUG', 'ALL']) or (parameters.debug > 0):
2101                logger.setLevel(logging.DEBUG)
2102
2103        elif parameters.log_level in ['INFO'] or parameters.verbose:
2104                logger.setLevel(logging.INFO)
2105
2106        elif parameters.log_level in ['WARNING']:
2107                logger.setLevel(logging.WARNING)
2108
2109        elif parameters.log_level in ['ERROR']:
2110                logger.setLevel(logging.ERROR)
2111
2112        elif parameters.log_level in ['CRITICAL']:
2113                logger.setLevel(logging.CRITICAL)
2114
2115        else:
2116                logger.setLevel(logging.INFO)
2117
2118        return logger
2119
2120
[22]2121if __name__ == '__main__':
2122        # Default config file
2123        #
[24]2124        configfile = '@email2trac_conf@'
[22]2125        project = ''
2126        component = ''
[202]2127        ticket_prefix = 'default'
[204]2128        dry_run = None
[317]2129        verbose = None
[414]2130        debug_interactive = None
[202]2131
[412]2132        SHORT_OPT = 'cdhf:np:t:v'
2133        LONG_OPT  =  ['component=', 'debug', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'verbose']
[201]2134
[22]2135        try:
[201]2136                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
[22]2137        except getopt.error,detail:
2138                print __doc__
2139                print detail
2140                sys.exit(1)
[87]2141       
[22]2142        project_name = None
2143        for opt,value in opts:
2144                if opt in [ '-h', '--help']:
2145                        print __doc__
2146                        sys.exit(0)
2147                elif opt in ['-c', '--component']:
2148                        component = value
[412]2149                elif opt in ['-d', '--debug']:
[414]2150                        debug_interactive = 1
[22]2151                elif opt in ['-f', '--file']:
2152                        configfile = value
[201]2153                elif opt in ['-n', '--dry-run']:
[204]2154                        dry_run = True
[22]2155                elif opt in ['-p', '--project']:
2156                        project_name = value
[202]2157                elif opt in ['-t', '--ticket_prefix']:
2158                        ticket_prefix = value
[388]2159                elif opt in ['-v', '--verbose']:
[317]2160                        verbose = True
[87]2161       
[22]2162        settings = ReadConfig(configfile, project_name)
[410]2163
2164        # The default prefix for ticket values in email2trac.conf
2165        #
[412]2166        settings.ticket_prefix = ticket_prefix
2167        settings.dry_run = dry_run
2168        settings.verbose = verbose
[410]2169
[414]2170        if not settings.debug and debug_interactive:
2171                settings.debug = debug_interactive
[412]2172
[419]2173        if not settings.project:
[22]2174                print __doc__
[79]2175                print 'No Trac project is defined in the email2trac config file.'
[22]2176                sys.exit(1)
[419]2177
2178        logger = setup_log(settings, os.path.basename(settings.project), debug_interactive)
[87]2179       
[22]2180        if component:
2181                settings['component'] = component
[202]2182
[363]2183        # Determine major trac version used to be in email2trac.conf
[373]2184        # Quick hack for 0.12
[363]2185        #
2186        version = '0.%s' %(trac_version.split('.')[1])
[373]2187        if version.startswith('0.12'):
2188                version = '0.12'
2189
[410]2190        logger.debug("Found trac version: %s" %(version))
[363]2191       
[87]2192        try:
[401]2193                if version == '0.10':
[87]2194                        from trac import attachment
2195                        from trac.env import Environment
2196                        from trac.ticket import Ticket
2197                        from trac.web.href import Href
2198                        from trac import util
[139]2199                        #
2200                        # return  util.text.to_unicode(str)
2201                        #
[87]2202                        # see http://projects.edgewall.com/trac/changeset/2799
2203                        from trac.ticket.notification import TicketNotifyEmail
[199]2204                        from trac import config as trac_config
[359]2205                        from trac.core import TracError
2206
[189]2207                elif version == '0.11':
[182]2208                        from trac import attachment
2209                        from trac.env import Environment
2210                        from trac.ticket import Ticket
2211                        from trac.web.href import Href
[199]2212                        from trac import config as trac_config
[182]2213                        from trac import util
[359]2214                        from trac.core import TracError
[397]2215                        from trac.perm import PermissionSystem
[260]2216
[368]2217                        #
2218                        # return  util.text.to_unicode(str)
2219                        #
2220                        # see http://projects.edgewall.com/trac/changeset/2799
2221                        from trac.ticket.notification import TicketNotifyEmail
[260]2222
[368]2223                elif version == '0.12':
2224                        from trac import attachment
2225                        from trac.env import Environment
2226                        from trac.ticket import Ticket
2227                        from trac.web.href import Href
2228                        from trac import config as trac_config
2229                        from trac import util
2230                        from trac.core import TracError
[397]2231                        from trac.perm import PermissionSystem
[368]2232
[182]2233                        #
2234                        # return  util.text.to_unicode(str)
2235                        #
2236                        # see http://projects.edgewall.com/trac/changeset/2799
2237                        from trac.ticket.notification import TicketNotifyEmail
[368]2238
2239
[189]2240                else:
[410]2241                        logger.error('TRAC version %s is not supported' %version)
[189]2242                        sys.exit(1)
[182]2243
[291]2244                # Must be set before environment is created
2245                #
2246                if settings.has_key('python_egg_cache'):
2247                        python_egg_cache = str(settings['python_egg_cache'])
2248                        os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
2249
[410]2250                if settings.debug > 0:
2251                        logger.debug('Loading environment %s', settings.project)
[359]2252
[87]2253                env = Environment(settings['project'], create=0)
[333]2254
[410]2255                tktparser = TicketEmailParser(env, settings, logger, float(version))
[87]2256                tktparser.parse(sys.stdin)
[22]2257
[464]2258        ## Catch all errors and use the logging module
[87]2259        #
2260        except Exception, error:
[187]2261
[411]2262                etype, evalue, etb = sys.exc_info()
2263                for e in traceback.format_exception(etype, evalue, etb):
2264                        logger.critical(e)
[187]2265
[97]2266                if m:
[98]2267                        tktparser.save_email_for_debug(m, True)
[97]2268
[249]2269                sys.exit(1)
[22]2270# EOB
Note: See TracBrowser for help on using the repository browser.