source: trunk/email2trac.py.in @ 498

Last change on this file since 498 was 498, checked in by bas, 13 years ago

added support for html to text conversion, in email2trac.conf:

  • html2text_cmd: /usr/bin/html2text -nobs

see #152

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