source: trunk/email2trac.py.in @ 316

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

Fixed a bug when there is no subject line, closes #179

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 42.4 KB
RevLine 
[22]1#!@PYTHON@
2# Copyright (C) 2002
3#
4# This file is part of the email2trac utils
5#
6# This program is free software; you can redistribute it and/or modify it
7# under the terms of the GNU General Public License as published by the
8# Free Software Foundation; either version 2, or (at your option) any
9# later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
19#
[80]20# For vi/emacs or other use tabstop=4 (vi: set ts=4)
21#
[22]22"""
[54]23email2trac.py -- Email tickets to Trac.
[22]24
25A simple MTA filter to create Trac tickets from inbound emails.
26
27Copyright 2005, Daniel Lundin <daniel@edgewall.com>
28Copyright 2005, Edgewall Software
29
[282]30Authors:
31  Bas van der Vlies <basv@sara.nl>
32  Walter de Jong <walter@sara.nl>
[22]33
34The scripts reads emails from stdin and inserts directly into a Trac database.
35
36How to use
37----------
[282]38 * See https://subtrac.sara.nl/oss/email2trac/
39
[22]40 * Create an config file:
[282]41    [DEFAULT]                        # REQUIRED
42    project      : /data/trac/test   # REQUIRED
43    debug        : 1                 # OPTIONAL, if set print some DEBUG info
[297]44    trac_version : 0.10              # OPTIONAL, default is 0.11
[22]45
[282]46    [jouvin]                         # OPTIONAL project declaration, if set both fields necessary
47    project      : /data/trac/jouvin # use -p|--project jouvin. 
[22]48       
49 * default config file is : /etc/email2trac.conf
50
51 * Commandline opions:
[205]52                -h,--help
53                -f,--file  <configuration file>
54                -n,--dry-run
55                -p, --project <project name>
56                -t, --ticket_prefix <name>
[22]57
58SVN Info:
59        $Id: email2trac.py.in 316 2010-02-15 10:44:25Z bas $
60"""
61import os
62import sys
63import string
64import getopt
65import stat
66import time
67import email
[136]68import email.Iterators
69import email.Header
[22]70import re
71import urllib
72import unicodedata
73from stat import *
74import mimetypes
[96]75import traceback
[22]76
[190]77
78# Will fail where unavailable, e.g. Windows
79#
80try:
81    import syslog
82    SYSLOG_AVAILABLE = True
83except ImportError:
84    SYSLOG_AVAILABLE = False
85
[182]86from datetime import tzinfo, timedelta, datetime
[199]87from trac import config as trac_config
[91]88
[96]89# Some global variables
90#
[276]91trac_default_version = '0.11'
[96]92m = None
[22]93
[182]94# A UTC class needed for trac version 0.11, added by
95# tbaschak at ktc dot mb dot ca
96#
97class UTC(tzinfo):
98        """UTC"""
99        ZERO = timedelta(0)
100        HOUR = timedelta(hours=1)
101       
102        def utcoffset(self, dt):
103                return self.ZERO
104               
105        def tzname(self, dt):
106                return "UTC"
107               
108        def dst(self, dt):
109                return self.ZERO
110
111
[22]112class TicketEmailParser(object):
113        env = None
114        comment = '> '
115   
[206]116        def __init__(self, env, parameters, version):
[22]117                self.env = env
118
119                # Database connection
120                #
121                self.db = None
122
[206]123                # Save parameters
124                #
125                self.parameters = parameters
126
[72]127                # Some useful mail constants
128                #
[287]129                self.email_name = None
[72]130                self.email_addr = None
[183]131                self.email_from = None
[288]132                self.author     = None
[287]133                self.id         = None
[294]134               
135                self.STRIP_CONTENT_TYPES = list()
[72]136
[22]137                self.VERSION = version
[206]138                self.DRY_RUN = parameters['dry_run']
[204]139
[172]140                self.get_config = self.env.config.get
[22]141
142                if parameters.has_key('umask'):
143                        os.umask(int(parameters['umask'], 8))
144
145                if parameters.has_key('debug'):
146                        self.DEBUG = int(parameters['debug'])
147                else:
148                        self.DEBUG = 0
149
150                if parameters.has_key('mailto_link'):
151                        self.MAILTO = int(parameters['mailto_link'])
[74]152                        if parameters.has_key('mailto_cc'):
153                                self.MAILTO_CC = parameters['mailto_cc']
154                        else:
155                                self.MAILTO_CC = ''
[22]156                else:
157                        self.MAILTO = 0
158
159                if parameters.has_key('spam_level'):
160                        self.SPAM_LEVEL = int(parameters['spam_level'])
161                else:
162                        self.SPAM_LEVEL = 0
163
[207]164                if parameters.has_key('spam_header'):
165                        self.SPAM_HEADER = parameters['spam_header']
166                else:
167                        self.SPAM_HEADER = 'X-Spam-Score'
168
[191]169                if parameters.has_key('email_quote'):
170                        self.EMAIL_QUOTE = str(parameters['email_quote'])
171                else:   
172                        self.EMAIL_QUOTE = '> '
[22]173
174                if parameters.has_key('email_header'):
175                        self.EMAIL_HEADER = int(parameters['email_header'])
176                else:
177                        self.EMAIL_HEADER = 0
178
[42]179                if parameters.has_key('alternate_notify_template'):
180                        self.notify_template = str(parameters['alternate_notify_template'])
181                else:
182                        self.notify_template = None
[22]183
[222]184                if parameters.has_key('alternate_notify_template_update'):
185                        self.notify_template_update = str(parameters['alternate_notify_template_update'])
186                else:
187                        self.notify_template_update = None
188
[43]189                if parameters.has_key('reply_all'):
190                        self.REPLY_ALL = int(parameters['reply_all'])
191                else:
192                        self.REPLY_ALL = 0
[42]193
[74]194                if parameters.has_key('ticket_update'):
195                        self.TICKET_UPDATE = int(parameters['ticket_update'])
196                else:
197                        self.TICKET_UPDATE = 0
[43]198
[118]199                if parameters.has_key('drop_spam'):
200                        self.DROP_SPAM = int(parameters['drop_spam'])
201                else:
202                        self.DROP_SPAM = 0
[74]203
[134]204                if parameters.has_key('verbatim_format'):
205                        self.VERBATIM_FORMAT = int(parameters['verbatim_format'])
206                else:
207                        self.VERBATIM_FORMAT = 1
[118]208
[231]209                if parameters.has_key('reflow'):
[256]210                        self.REFLOW = int(parameters['reflow'])
[231]211                else:
212                        self.REFLOW = 1
213
[278]214                if parameters.has_key('drop_alternative_html_version'):
215                        self.DROP_ALTERNATIVE_HTML_VERSION = int(parameters['drop_alternative_html_version'])
216                else:
217                        self.DROP_ALTERNATIVE_HTML_VERSION = 0
218
[136]219                if parameters.has_key('strip_signature'):
220                        self.STRIP_SIGNATURE = int(parameters['strip_signature'])
221                else:
222                        self.STRIP_SIGNATURE = 0
[134]223
[191]224                if parameters.has_key('strip_quotes'):
225                        self.STRIP_QUOTES = int(parameters['strip_quotes'])
226                else:
227                        self.STRIP_QUOTES = 0
228
[309]229                self.properties = dict()
230                if parameters.has_key('inline_properties'):
231                        self.INLINE_PROPERTIES = int(parameters['inline_properties'])
232                else:
233                        self.INLINE_PROPERTIES = 0
234
[148]235                if parameters.has_key('use_textwrap'):
236                        self.USE_TEXTWRAP = int(parameters['use_textwrap'])
237                else:
238                        self.USE_TEXTWRAP = 0
239
[238]240                if parameters.has_key('binhex'):
[294]241                        self.STRIP_CONTENT_TYPES.append('application/mac-binhex40')
[238]242
243                if parameters.has_key('applesingle'):
[294]244                        self.STRIP_CONTENT_TYPES.append('application/applefile')
[238]245
246                if parameters.has_key('appledouble'):
[294]247                        self.STRIP_CONTENT_TYPES.append('application/applefile')
[238]248
[294]249                if parameters.has_key('strip_content_types'):
250                        items = parameters['strip_content_types'].split(',')
251                        for item in items:
252                                self.STRIP_CONTENT_TYPES.append(item.strip())
253
[257]254                self.WORKFLOW = None
255                if parameters.has_key('workflow'):
256                        self.WORKFLOW = parameters['workflow']
257
[173]258                # Use OS independend functions
259                #
260                self.TMPDIR = os.path.normcase('/tmp')
261                if parameters.has_key('tmpdir'):
262                        self.TMPDIR = os.path.normcase(str(parameters['tmpdir']))
263
[194]264                if parameters.has_key('ignore_trac_user_settings'):
265                        self.IGNORE_TRAC_USER_SETTINGS = int(parameters['ignore_trac_user_settings'])
266                else:
267                        self.IGNORE_TRAC_USER_SETTINGS = 0
[191]268
[297]269                if parameters.has_key('subject_field_separator'):
270                        self.SUBJECT_FIELD_SEPARATOR = parameters['subject_field_separator'].strip()
271                else:
272                        self.SUBJECT_FIELD_SEPARATOR = '&'
273
[305]274                self.trac_smtp_from = self.get_config('notification', 'smtp_from')
275
[22]276        def spam(self, message):
[191]277                """
278                # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
279                # Note if Spam_level then '*' are included
280                """
[194]281                spam = False
[207]282                if message.has_key(self.SPAM_HEADER):
283                        spam_l = string.split(message[self.SPAM_HEADER])
[22]284
[207]285                        try:
286                                number = spam_l[0].count('*')
287                        except IndexError, detail:
288                                number = 0
289                               
[22]290                        if number >= self.SPAM_LEVEL:
[194]291                                spam = True
292                               
[191]293                # treat virus mails as spam
294                #
295                elif message.has_key('X-Virus-found'):                 
[194]296                        spam = True
297
298                # How to handle SPAM messages
299                #
300                if self.DROP_SPAM and spam:
301                        if self.DEBUG > 2 :
302                                print 'This message is a SPAM. Automatic ticket insertion refused (SPAM level > %d' % self.SPAM_LEVEL
303
[204]304                        return 'drop'   
[194]305
306                elif spam:
307
[204]308                        return 'Spam'   
[67]309
[194]310                else:
[22]311
[204]312                        return False
[191]313
[221]314        def email_header_acl(self, keyword, header_field, default):
[206]315                """
[221]316                This function wil check if the email address is allowed or denied
317                to send mail to the ticket list
318            """
[206]319                try:
[221]320                        mail_addresses = self.parameters[keyword]
321
322                        # Check if we have an empty string
323                        #
324                        if not mail_addresses:
325                                return default
326
[206]327                except KeyError, detail:
[221]328                        if self.DEBUG > 2 :
[250]329                                print 'TD: %s not defined, all messages are allowed.' %(keyword)
[206]330
[221]331                        return default
[206]332
[221]333                mail_addresses = string.split(mail_addresses, ',')
334
335                for entry in mail_addresses:
[209]336                        entry = entry.strip()
[221]337                        TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
338                        result =  TO_RE.search(header_field)
[208]339                        if result:
340                                return True
[149]341
[208]342                return False
343
[139]344        def email_to_unicode(self, message_str):
[22]345                """
346                Email has 7 bit ASCII code, convert it to unicode with the charset
[79]347        that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
[22]348                understands it.
349                """
[139]350                results =  email.Header.decode_header(message_str)
[288]351                s = None
[22]352                for text,format in results:
353                        if format:
354                                try:
355                                        temp = unicode(text, format)
[139]356                                except UnicodeError, detail:
[22]357                                        # This always works
358                                        #
359                                        temp = unicode(text, 'iso-8859-15')
[139]360                                except LookupError, detail:
361                                        #text = 'ERROR: Could not find charset: %s, please install' %format
362                                        #temp = unicode(text, 'iso-8859-15')
363                                        temp = message_str
364                                       
[22]365                        else:
366                                temp = string.strip(text)
[92]367                                temp = unicode(text, 'iso-8859-15')
[22]368
[288]369                        if s:
370                                s = '%s %s' %(s, temp)
[22]371                        else:
[288]372                                s = '%s' %temp
[22]373
[288]374                #s = s.encode('utf-8')
375                return s
[22]376
[236]377
[22]378        def email_header_txt(self, m):
[72]379                """
380                Display To and CC addresses in description field
381                """
[288]382                s = ''
[213]383                #if m['To'] and len(m['To']) > 0 and m['To'] != 'hic@sara.nl':
384                if m['To'] and len(m['To']) > 0:
[288]385                        s = "'''To:''' %s\r\n" %(m['To'])
[22]386                if m['Cc'] and len(m['Cc']) > 0:
[288]387                        s = "%s'''Cc:''' %s\r\n" % (s, m['Cc'])
[22]388
[288]389                return  self.email_to_unicode(s)
[22]390
[138]391
[194]392        def get_sender_info(self, message):
[45]393                """
[72]394                Get the default author name and email address from the message
[226]395                """
[43]396
[226]397                self.email_to = self.email_to_unicode(message['to'])
398                self.to_name, self.to_email_addr = email.Utils.parseaddr (self.email_to)
399
[194]400                self.email_from = self.email_to_unicode(message['from'])
[287]401                self.email_name, self.email_addr  = email.Utils.parseaddr(self.email_from)
[142]402
[304]403                ## Trac can not handle author's name that contains spaces
404                #  and forbid the ticket email address as author field
[194]405
[305]406                if self.email_addr == self.trac_smtp_from:
[304]407                        self.author = "email2trac"
408                else:
409                        self.author = self.email_addr
410
[194]411                if self.IGNORE_TRAC_USER_SETTINGS:
412                        return
413
414                # Is this a registered user, use email address as search key:
415                # result:
416                #   u : login name
417                #   n : Name that the user has set in the settings tab
418                #   e : email address that the user has set in the settings tab
[45]419                #
[194]420                users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
[250]421                        if e and (e.lower() == self.email_addr.lower()) ]
[43]422
[45]423                if len(users) == 1:
[194]424                        self.email_from = users[0][0]
[250]425                        self.author = users[0][0]
[45]426
[72]427        def set_reply_fields(self, ticket, message):
428                """
429                Set all the right fields for a new ticket
430                """
[299]431                if self.DEBUG:
432                        print 'TD: set_reply_fields'
[72]433
[270]434                ## Only use name or email adress
435                #ticket['reporter'] = self.email_from
436                ticket['reporter'] = self.author
437
438
[45]439                # Put all CC-addresses in ticket CC field
[43]440                #
441                if self.REPLY_ALL:
442
[299]443                        email_cc = ''
444
445                        cc_addrs = email.Utils.getaddresses( message.get_all('cc', []) )
446
447                        if not cc_addrs:
[105]448                                return
[43]449
[299]450                        ## Build a list of forbidden CC addresses
451                        #
[300]452                        #to_addrs = email.Utils.getaddresses( message.get_all('to', []) )
453                        #to_list = list()
454                        #for n,e in to_addrs:
455                        #       to_list.append(e)
[299]456                               
[43]457                        # Remove reporter email address if notification is
458                        # on
459                        #
460                        if self.notification:
461                                try:
[299]462                                        cc_addrs.remove((self.author, self.email_addr))
[43]463                                except ValueError, detail:
464                                        pass
465
[299]466                        for name,addr in cc_addrs:
467               
468                                ## Prevent mail loop
469                                #
[300]470                                #if addr in to_list:
[304]471
472                                if addr == self.trac_smtp_from:
[299]473                                        if self.DEBUG:
474                                                print "Skipping %s mail address for CC-field" %(addr)
475                                        continue
[43]476
[299]477                                if email_cc:
478                                        email_cc = '%s, %s' %(email_cc, addr)
479                                else:
480                                        email_cc = addr
[96]481
[299]482                        if email_cc:
483                                if self.DEBUG:
484                                        print 'TD: set_reply_fields: %s' %email_cc
485
486                                ticket['cc'] = self.email_to_unicode(email_cc)
487
[310]488        def debug_body(self, message_body, tempfile=False):
489                if tempfile:
490                        import tempfile
491                        body_file = tempfile.mktemp('.email2trac')
492                else:
493                        body_file = os.path.join(self.TMPDIR, 'body.txt')
494
495                print 'TD: writing body (%s)' % body_file
496                fx = open(body_file, 'wb')
497                if not message_body:
498                        message_body = '(None)'
499
500                message_body = message_body.encode('utf-8')
501                #message_body = unicode(message_body, 'iso-8859-15')
502
503                fx.write(message_body)
504                fx.close()
505                try:
506                        os.chmod(body_file,S_IRWXU|S_IRWXG|S_IRWXO)
507                except OSError:
508                        pass
509
510        def debug_attachments(self, message_parts):
511                n = 0
512                for part in message_parts:
513                        # Skip inline text parts
514                        if not isinstance(part, tuple):
515                                continue
516                               
517                        (original, filename, part) = part
518
519                        n = n + 1
520                        print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type())
521                        print 'TD: part%d: filename: %s' % (n, part.get_filename())
522
523                        part_file = os.path.join(self.TMPDIR, filename)
524                        #part_file = '/var/tmp/part%d' % n
525                        print 'TD: writing part%d (%s)' % (n,part_file)
526                        fx = open(part_file, 'wb')
527                        text = part.get_payload(decode=1)
528                        if not text:
529                                text = '(None)'
530                        fx.write(text)
531                        fx.close()
532                        try:
533                                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
534                        except OSError:
535                                pass
536
[96]537        def save_email_for_debug(self, message, tempfile=False):
[309]538
[96]539                if tempfile:
540                        import tempfile
541                        msg_file = tempfile.mktemp('.email2trac')
542                else:
[173]543                        #msg_file = '/var/tmp/msg.txt'
544                        msg_file = os.path.join(self.TMPDIR, 'msg.txt')
545
[44]546                print 'TD: saving email to %s' % msg_file
547                fx = open(msg_file, 'wb')
548                fx.write('%s' % message)
549                fx.close()
550                try:
551                        os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
552                except OSError:
553                        pass
554
[309]555                message_parts = self.get_message_parts(message)
556                message_parts = self.unique_attachment_names(message_parts)
557                body_text = self.body_text(message_parts)
558                self.debug_body(body_text, True)
559                self.debug_attachments(message_parts)
560
[288]561        def str_to_dict(self, s):
[164]562                """
[288]563                Transfrom a string of the form [<key>=<value>]+ to dict[<key>] = <value>
[164]564                """
565
[297]566                fields = string.split(s, self.SUBJECT_FIELD_SEPARATOR)
[262]567
[164]568                result = dict()
569                for field in fields:
570                        try:
[262]571                                index, value = string.split(field, '=')
[169]572
573                                # We can not change the description of a ticket via the subject
574                                # line. The description is the body of the email
575                                #
576                                if index.lower() in ['description']:
577                                        continue
578
[164]579                                if value:
[165]580                                        result[index.lower()] = value
[169]581
[164]582                        except ValueError:
583                                pass
[165]584                return result
[167]585
[202]586        def update_ticket_fields(self, ticket, user_dict, use_default=None):
587                """
588                This will update the ticket fields. It will check if the
589                given fields are known and if the right values are specified
590                It will only update the ticket field value:
[169]591                        - If the field is known
[202]592                        - If the value supplied is valid for the ticket field.
593                          If not then there are two options:
594                           1) Skip the value (use_default=None)
595                           2) Set default value for field (use_default=1)
[169]596                """
[301]597                if self.DEBUG:
598                        print "TD: update_ticket_fields"
[169]599
600                # Build a system dictionary from the ticket fields
601                # with field as index and option as value
602                #
603                sys_dict = dict()
604                for field in ticket.fields:
[167]605                        try:
[169]606                                sys_dict[field['name']] = field['options']
607
[167]608                        except KeyError:
[169]609                                sys_dict[field['name']] = None
[167]610                                pass
[169]611
[301]612                ## Check user supplied fields an compare them with the
[169]613                # system one's
614                #
615                for field,value in user_dict.items():
[202]616                        if self.DEBUG >= 10:
617                                print  'user_field\t %s = %s' %(field,value)
[169]618
[301]619                        ## To prevent mail loop
620                        #
621                        if field == 'cc':
622
623                                cc_list = user_dict['cc'].split(',')
624
[304]625                                if self.trac_smtp_from in cc_list:
[301]626                                        if self.DEBUG > 10:
[304]627                                                print 'TD: MAIL LOOP: %s is not allowed as CC address' %(self.trac_smtp_from)
628                                        cc_list.remove(self.trac_smtp_from)
[301]629
630                                value = ','.join(cc_list)
631                               
632
[169]633                        if sys_dict.has_key(field):
634
635                                # Check if value is an allowed system option, if TypeError then
636                                # every value is allowed
637                                #
638                                try:
639                                        if value in sys_dict[field]:
640                                                ticket[field] = value
[202]641                                        else:
642                                                # Must we set a default if value is not allowed
643                                                #
644                                                if use_default:
645                                                        value = self.get_config('ticket', 'default_%s' %(field) )
646                                                        ticket[field] = value
[169]647
648                                except TypeError:
649                                        ticket[field] = value
[202]650
651                                if self.DEBUG >= 10:
652                                        print  'ticket_field\t %s = %s' %(field,  ticket[field])
[169]653                                       
[260]654        def ticket_update(self, m, id, spam):
[78]655                """
[79]656                If the current email is a reply to an existing ticket, this function
657                will append the contents of this email to that ticket, instead of
658                creating a new one.
[78]659                """
[250]660                if self.DEBUG:
[260]661                        print "TD: ticket_update: %s" %id
[202]662
[164]663                # Must we update ticket fields
664                #
[220]665                update_fields = dict()
[165]666                try:
[260]667                        id, keywords = string.split(id, '?')
[262]668
669                        # Skip the last ':' character
670                        #
671                        keywords = keywords[:-1]
[220]672                        update_fields = self.str_to_dict(keywords)
[165]673
674                        # Strip '#'
675                        #
[260]676                        self.id = int(id[1:])
[165]677
[260]678                except ValueError:
[165]679                        # Strip '#' and ':'
680                        #
[260]681                        self.id = int(id[1:-1])
[164]682
[71]683
[194]684                # When is the change committed
685                #
686                if self.VERSION == 0.11:
687                        utc = UTC()
688                        when = datetime.now(utc)
689                else:
690                        when = int(time.time())
[77]691
[172]692                try:
[253]693                        tkt = Ticket(self.env, self.id, self.db)
[172]694                except util.TracError, detail:
[253]695                        # Not a valid ticket
696                        self.id = None
[172]697                        return False
[126]698
[288]699                # How many changes has this ticket
700                cnum = len(tkt.get_changelog())
701
702
[220]703                # reopen the ticket if it is was closed
704                # We must use the ticket workflow framework
705                #
706                if tkt['status'] in ['closed']:
707
[257]708                        #print controller.actions['reopen']
709                        #
710                        # As reference 
711                        # req = Mock(href=Href('/'), abs_href=Href('http://www.example.com/'), authname='anonymous', perm=MockPerm(), args={})
712                        #
713                        #a = controller.render_ticket_action_control(req, tkt, 'reopen')
714                        #print 'controller : ', a
715                        #
716                        #b = controller.get_all_status()
717                        #print 'get all status: ', b
718                        #
719                        #b = controller.get_ticket_changes(req, tkt, 'reopen')
720                        #print 'get_ticket_changes :', b
721
[288]722                        if self.WORKFLOW and (self.VERSION in [0.11]) :
[257]723                                from trac.ticket.default_workflow import ConfigurableTicketWorkflow
724                                from trac.test import Mock, MockPerm
725
726                                req = Mock(authname='anonymous', perm=MockPerm(), args={})
727
728                                controller = ConfigurableTicketWorkflow(self.env)
729                                fields = controller.get_ticket_changes(req, tkt, self.WORKFLOW)
730
731                                if self.DEBUG:
732                                        print 'TD: Workflow ticket update fields: ', fields
733
734                                for key in fields.keys():
735                                        tkt[key] = fields[key]
736
737                        else:
738                                tkt['status'] = 'reopened'
739                                tkt['resolution'] = ''
740
[309]741                # Must we update some ticket fields properties via subjectline
[172]742                #
[220]743                if update_fields:
744                        self.update_ticket_fields(tkt, update_fields)
[166]745
[236]746                message_parts = self.get_message_parts(m)
[253]747                message_parts = self.unique_attachment_names(message_parts)
[210]748
[309]749                # Must we update some ticket fields properties via body_text
750                #
751                if self.properties:
752                                self.update_ticket_fields(tkt, self.properties)
753
[177]754                if self.EMAIL_HEADER:
[236]755                        message_parts.insert(0, self.email_header_txt(m))
[76]756
[236]757                body_text = self.body_text(message_parts)
758
[309]759                if body_text.strip() or update_fields or self.properties:
[250]760                        if self.DRY_RUN:
[288]761                                print 'DRY_RUN: tkt.save_changes(self.author, body_text, ticket_change_number) ', self.author, cnum
[250]762                        else:
[288]763                                tkt.save_changes(self.author, body_text, when, None, str(cnum))
764                       
[219]765
[129]766                if self.VERSION  == 0.9:
[288]767                        s = self.attachments(message_parts, True)
[129]768                else:
[288]769                        s = self.attachments(message_parts)
[76]770
[204]771                if self.notification and not spam:
[253]772                        self.notify(tkt, False, when)
[72]773
[71]774                return True
775
[202]776        def set_ticket_fields(self, ticket):
[77]777                """
[202]778                set the ticket fields to value specified
779                        - /etc/email2trac.conf with <prefix>_<field>
780                        - trac default values, trac.ini
781                """
782                user_dict = dict()
783
784                for field in ticket.fields:
785
786                        name = field['name']
787
[215]788                        # skip some fields like resolution
789                        #
790                        if name in [ 'resolution' ]:
791                                continue
792
[202]793                        # default trac value
794                        #
[233]795                        if not field.get('custom'):
796                                value = self.get_config('ticket', 'default_%s' %(name) )
797                        else:
798                                value = field.get('value')
799                                options = field.get('options')
[234]800                                if value and options and value not in options:
[233]801                                        value = options[int(value)]
802
[202]803                        if self.DEBUG > 10:
804                                print 'trac.ini name %s = %s' %(name, value)
805
[206]806                        prefix = self.parameters['ticket_prefix']
[202]807                        try:
[206]808                                value = self.parameters['%s_%s' %(prefix, name)]
[202]809                                if self.DEBUG > 10:
810                                        print 'email2trac.conf %s = %s ' %(name, value)
811
812                        except KeyError, detail:
813                                pass
814               
815                        if self.DEBUG:
816                                print 'user_dict[%s] = %s' %(name, value)
817
818                        user_dict[name] = value
819
820                self.update_ticket_fields(ticket, user_dict, use_default=1)
821
822                # Set status ticket
823                #`
824                ticket['status'] = 'new'
825
826
827
[262]828        def new_ticket(self, msg, subject, spam, set_fields = None):
[202]829                """
[77]830                Create a new ticket
831                """
[250]832                if self.DEBUG:
833                        print "TD: new_ticket"
834
[41]835                tkt = Ticket(self.env)
[202]836                self.set_ticket_fields(tkt)
837
838                # Old style setting for component, will be removed
839                #
[204]840                if spam:
841                        tkt['component'] = 'Spam'
842
[206]843                elif self.parameters.has_key('component'):
844                        tkt['component'] = self.parameters['component']
[201]845
[22]846                if not msg['Subject']:
[151]847                        tkt['summary'] = u'(No subject)'
[22]848                else:
[264]849                        tkt['summary'] = subject
[22]850
[72]851                self.set_reply_fields(tkt, msg)
[22]852
[262]853                if set_fields:
854                        rest, keywords = string.split(set_fields, '?')
855
856                        if keywords:
857                                update_fields = self.str_to_dict(keywords)
858                                self.update_ticket_fields(tkt, update_fields)
859
[45]860                # produce e-mail like header
861                #
[22]862                head = ''
863                if self.EMAIL_HEADER > 0:
864                        head = self.email_header_txt(msg)
[296]865
[236]866                message_parts = self.get_message_parts(msg)
[309]867
868                # Must we update some ticket fields properties via body_text
869                #
870                if self.properties:
871                                self.update_ticket_fields(tkt, self.properties)
872
[296]873                if self.DEBUG:
874                        print 'TD: self.get_message_parts ',
875                        print message_parts
876
[236]877                message_parts = self.unique_attachment_names(message_parts)
[296]878                if self.DEBUG:
879                        print 'TD: self.unique_attachment_names',
880                        print message_parts
[236]881               
882                if self.EMAIL_HEADER > 0:
883                        message_parts.insert(0, self.email_header_txt(msg))
884                       
885                body_text = self.body_text(message_parts)
[45]886
[236]887                tkt['description'] = body_text
[90]888
[182]889                #when = int(time.time())
[192]890                #
[182]891                utc = UTC()
892                when = datetime.now(utc)
[45]893
[253]894                if not self.DRY_RUN:
895                        self.id = tkt.insert()
[273]896       
[90]897                changed = False
898                comment = ''
[77]899
[273]900                # some routines in trac are dependend on ticket id     
901                # like alternate notify template
902                #
903                if self.notify_template:
[274]904                        tkt['id'] = self.id
[273]905                        changed = True
906
[295]907                ## Rewrite the description if we have mailto enabled
[45]908                #
[72]909                if self.MAILTO:
[100]910                        changed = True
[142]911                        comment = u'\nadded mailto line\n'
[253]912                        mailto = self.html_mailto_link( m['Subject'], body_text)
913
[213]914                        tkt['description'] = u'%s\r\n%s%s\r\n' \
[142]915                                %(head, mailto, body_text)
[295]916       
917                ## Save the attachments to the ticket   
918                #
919                has_attachments =  self.attachments(message_parts)
920
[292]921                #  Disabled
922                #if has_attachments:
923                #       changed = True
924                #       comment = '%s\n%s\n' %(comment, has_attachments)
[45]925
[90]926                if changed:
[204]927                        if self.DRY_RUN:
[250]928                                print 'DRY_RUN: tkt.save_changes(self.author, comment) ', self.author
[201]929                        else:
930                                tkt.save_changes(self.author, comment)
931                                #print tkt.get_changelog(self.db, when)
[90]932
[250]933                if self.notification and not spam:
[253]934                        self.notify(tkt, True)
[45]935
[260]936
937        def blog(self, id):
938                """
939                The blog create/update function
940                """
941                # import the modules
942                #
943                from tracfullblog.core import FullBlogCore
[312]944                from tracfullblog.model import BlogPost, BlogComment
945                from trac.test import Mock, MockPerm
[260]946
947                # instantiate blog core
948                blog = FullBlogCore(self.env)
[312]949                req = Mock(authname='anonymous', perm=MockPerm(), args={})
950
[260]951                if id:
952
953                        # update blog
954                        #
[268]955                        comment = BlogComment(self.env, id)
[260]956                        comment.author = self.author
[312]957
958                        message_parts = self.get_message_parts(m)
959                        comment.comment = self.body_text(message_parts)
960
[260]961                        blog.create_comment(req, comment)
962
963                else:
964                        # create blog
965                        #
966                        import time
967                        post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime()))
968
969                        #post = BlogPost(self.env, blog._get_default_postname(self.env))
970                       
971                        post.author = self.author
972                        post.title = self.email_to_unicode(m['Subject'])
[312]973
974                        message_parts = self.get_message_parts(m)
975                        post.body = self.body_text(message_parts)
[260]976                       
977                        blog.create_post(req, post, self.author, u'Created by email2trac', False)
978
[309]979        def save_message_for_debug(self):
980                """
981                Save the messages. So we can use it for debug purposes
982                """
983               
[260]984
[77]985        def parse(self, fp):
[96]986                global m
987
[77]988                m = email.message_from_file(fp)
[239]989               
[77]990                if not m:
[221]991                        if self.DEBUG:
[250]992                                print "TD: This is not a valid email message format"
[77]993                        return
[239]994                       
995                # Work around lack of header folding in Python; see http://bugs.python.org/issue4696
[316]996                try:
997                        m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
998                except AttributeError, detail:
999                        pass
[239]1000
[77]1001                if self.DEBUG > 1:        # save the entire e-mail message text
[219]1002                        self.save_email_for_debug(m, True)
[77]1003
1004                self.db = self.env.get_db_cnx()
[194]1005                self.get_sender_info(m)
[152]1006
[221]1007                if not self.email_header_acl('white_list', self.email_addr, True):
1008                        if self.DEBUG > 1 :
1009                                print 'Message rejected : %s not in white list' %(self.email_addr)
1010                        return False
[77]1011
[221]1012                if self.email_header_acl('black_list', self.email_addr, False):
1013                        if self.DEBUG > 1 :
1014                                print 'Message rejected : %s in black list' %(self.email_addr)
1015                        return False
1016
[227]1017                if not self.email_header_acl('recipient_list', self.to_email_addr, True):
[226]1018                        if self.DEBUG > 1 :
1019                                print 'Message rejected : %s not in recipient list' %(self.to_email_addr)
1020                        return False
1021
[204]1022                # If drop the message
[194]1023                #
[204]1024                if self.spam(m) == 'drop':
[194]1025                        return False
1026
[204]1027                elif self.spam(m) == 'spam':
1028                        spam_msg = True
1029                else:
1030                        spam_msg = False
1031
[77]1032                if self.get_config('notification', 'smtp_enabled') in ['true']:
1033                        self.notification = 1
1034                else:
1035                        self.notification = 0
1036
[304]1037
[260]1038                # Check if  FullBlogPlugin is installed
[77]1039                #
[260]1040                blog_enabled = None
1041                if self.get_config('components', 'tracfullblog.*') in ['enabled']:
1042                        blog_enabled = True
1043                       
1044                if not m['Subject']:
1045                        return False
1046                else:
1047                        subject  = self.email_to_unicode(m['Subject'])         
[77]1048
[260]1049                #
1050                # [hic] #1529: Re: LRZ
1051                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
1052                #
1053                TICKET_RE = re.compile(r"""
1054                        (?P<blog>blog:(?P<blog_id>\w*))
[262]1055                        |(?P<new_fields>[#][?].*)
1056                        |(?P<reply>[#][\d]+:)
1057                        |(?P<reply_fields>[#][\d]+\?.*?:)
[260]1058                        """, re.VERBOSE)
[77]1059
[265]1060                # Find out if this is a ticket or a blog
1061                #
[260]1062                result =  TICKET_RE.search(subject)
1063
1064                if result:
[309]1065                        if result.group('blog'):
1066                                if blog_enabled:
1067                                        self.blog(result.group('blog_id'))
1068                                else:
1069                                        if self.DEBUG:
1070                                                print "Fullblog plugin is not installed"
1071                                                return
[260]1072
1073                        # update ticket + fields
1074                        #
[262]1075                        if result.group('reply_fields') and self.TICKET_UPDATE:
1076                                self.ticket_update(m, result.group('reply_fields'), spam_msg)
[260]1077
1078                        # Update ticket
1079                        #
[262]1080                        elif result.group('reply') and self.TICKET_UPDATE:
1081                                self.ticket_update(m, result.group('reply'), spam_msg)
[260]1082
[262]1083                        # New ticket + fields
1084                        #
1085                        elif result.group('new_fields'):
1086                                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
1087
[260]1088                # Create ticket
1089                #
1090                else:
[262]1091                        self.new_ticket(m, subject, spam_msg)
[260]1092
[136]1093        def strip_signature(self, text):
1094                """
1095                Strip signature from message, inspired by Mailman software
1096                """
1097                body = []
1098                for line in text.splitlines():
1099                        if line == '-- ':
1100                                break
1101                        body.append(line)
1102
1103                return ('\n'.join(body))
1104
[231]1105        def reflow(self, text, delsp = 0):
1106                """
1107                Reflow the message based on the format="flowed" specification (RFC 3676)
1108                """
1109                flowedlines = []
1110                quotelevel = 0
1111                prevflowed = 0
1112
1113                for line in text.splitlines():
1114                        from re import match
1115                       
1116                        # Figure out the quote level and the content of the current line
1117                        m = match('(>*)( ?)(.*)', line)
1118                        linequotelevel = len(m.group(1))
1119                        line = m.group(3)
1120
1121                        # Determine whether this line is flowed
1122                        if line and line != '-- ' and line[-1] == ' ':
1123                                flowed = 1
1124                        else:
1125                                flowed = 0
1126
1127                        if flowed and delsp and line and line[-1] == ' ':
1128                                line = line[:-1]
1129
1130                        # If the previous line is flowed, append this line to it
1131                        if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1132                                flowedlines[-1] += line
1133                        # Otherwise, start a new line
1134                        else:
1135                                flowedlines.append('>' * linequotelevel + line)
1136
1137                        prevflowed = flowed
1138                       
1139
1140                return '\n'.join(flowedlines)
1141
[191]1142        def strip_quotes(self, text):
[193]1143                """
1144                Strip quotes from message by Nicolas Mendoza
1145                """
1146                body = []
1147                for line in text.splitlines():
1148                        if line.startswith(self.EMAIL_QUOTE):
1149                                continue
1150                        body.append(line)
[151]1151
[193]1152                return ('\n'.join(body))
[191]1153
[309]1154        def inline_properties(self, text):
1155                """
1156                Parse text if we use inline keywords to set ticket fields
1157                """
1158                if self.DEBUG:
1159                        print 'TD: inline_properties function'
1160
1161                properties = dict()
1162                body = list()
1163
1164                INLINE_EXP = re.compile('\s*[@]\s*([a-zA-Z]+)\s*:(.*)$')
1165
1166                for line in text.splitlines():
1167                        match = INLINE_EXP.match(line)
1168                        if match:
1169                                keyword, value = match.groups()
1170                                self.properties[keyword] = value.strip()
[311]1171                                if self.DEBUG:
1172                                        print "TD: inline properties: %s : %s" %(keyword,value)
[309]1173                        else:
1174                                body.append(line)
1175                               
1176                return '\n'.join(body)
1177
1178
[154]1179        def wrap_text(self, text, replace_whitespace = False):
[151]1180                """
[191]1181                Will break a lines longer then given length into several small
1182                lines of size given length
[151]1183                """
1184                import textwrap
[154]1185
[151]1186                LINESEPARATOR = '\n'
[153]1187                reformat = ''
[151]1188
[154]1189                for s in text.split(LINESEPARATOR):
1190                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
1191                        if tmp:
1192                                reformat = '%s\n%s' %(reformat,tmp)
1193                        else:
1194                                reformat = '%s\n' %reformat
[153]1195
1196                return reformat
1197
[154]1198                # Python2.4 and higher
1199                #
1200                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1201                #
1202
1203
[236]1204        def get_message_parts(self, msg):
[45]1205                """
[236]1206                parses the email message and returns a list of body parts and attachments
1207                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
[45]1208                """
[309]1209                message_parts = list()
[294]1210       
[278]1211                ALTERNATIVE_MULTIPART = False
1212
[22]1213                for part in msg.walk():
[236]1214                        if self.DEBUG:
[278]1215                                print 'TD: Message part: Main-Type: %s' % part.get_content_maintype()
[236]1216                                print 'TD: Message part: Content-Type: %s' % part.get_content_type()
[278]1217
1218                        ## Check content type
[294]1219                        #
1220                        if part.get_content_type() in self.STRIP_CONTENT_TYPES:
[238]1221
[294]1222                                if self.DEBUG:
1223                                        print "TD: A %s attachment named '%s' was skipped" %(part.get_content_type(), part.get_filename())
[238]1224
1225                                continue
1226
[294]1227                        ## Catch some mulitpart execptions
1228                        #
1229                        if part.get_content_type() == 'multipart/alternative':
[278]1230                                ALTERNATIVE_MULTIPART = True
1231                                continue
1232
[294]1233                        ## Skip multipart containers
[278]1234                        #
[45]1235                        if part.get_content_maintype() == 'multipart':
[278]1236                                if self.DEBUG:
1237                                        print "TD: Skipping multipart container"
[22]1238                                continue
[278]1239                       
[294]1240                        ## 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"
1241                        #
[236]1242                        inline = self.inline_part(part)
1243
[294]1244                        ## Drop HTML message
1245                        #
[278]1246                        if ALTERNATIVE_MULTIPART and self.DROP_ALTERNATIVE_HTML_VERSION:
1247                                if part.get_content_type() == 'text/html':
1248                                        if self.DEBUG:
1249                                                print "TD: Skipping alternative HTML message"
1250
1251                                        ALTERNATIVE_MULTIPART = False
1252                                        continue
1253
[294]1254                        ## Inline text parts are where the body is
1255                        #
[236]1256                        if part.get_content_type() == 'text/plain' and inline:
1257                                if self.DEBUG:
1258                                        print 'TD:               Inline body part'
1259
[45]1260                                # Try to decode, if fails then do not decode
1261                                #
[90]1262                                body_text = part.get_payload(decode=1)
[45]1263                                if not body_text:                       
[90]1264                                        body_text = part.get_payload(decode=0)
[231]1265
[232]1266                                format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1267                                delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
[231]1268
1269                                if self.REFLOW and not self.VERBATIM_FORMAT and format == 'flowed':
1270                                        body_text = self.reflow(body_text, delsp == 'yes')
[154]1271       
[136]1272                                if self.STRIP_SIGNATURE:
1273                                        body_text = self.strip_signature(body_text)
[22]1274
[191]1275                                if self.STRIP_QUOTES:
1276                                        body_text = self.strip_quotes(body_text)
1277
[309]1278                                if self.INLINE_PROPERTIES:
1279                                        body_text = self.inline_properties(body_text)
1280
[148]1281                                if self.USE_TEXTWRAP:
[151]1282                                        body_text = self.wrap_text(body_text)
[148]1283
[294]1284                                ## Get contents charset (iso-8859-15 if not defined in mail headers)
[45]1285                                #
[100]1286                                charset = part.get_content_charset()
[102]1287                                if not charset:
1288                                        charset = 'iso-8859-15'
1289
[89]1290                                try:
[96]1291                                        ubody_text = unicode(body_text, charset)
[100]1292
1293                                except UnicodeError, detail:
[96]1294                                        ubody_text = unicode(body_text, 'iso-8859-15')
[89]1295
[100]1296                                except LookupError, detail:
[139]1297                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
[100]1298
[236]1299                                if self.VERBATIM_FORMAT:
1300                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1301                                else:
1302                                        message_parts.append('%s' %ubody_text)
1303                        else:
1304                                if self.DEBUG:
[315]1305                                        try:
1306                                                print 'TD:               Filename: %s' % part.get_filename()
1307                                        except UnicodeEncodeError, detail:
1308                                                print 'TD:               Filename: Can not be printed due to non-ascci characters'
[22]1309
[236]1310                                message_parts.append((part.get_filename(), part))
1311
1312                return message_parts
1313               
[253]1314        def unique_attachment_names(self, message_parts):
[296]1315                """
1316                """
[236]1317                renamed_parts = []
1318                attachment_names = set()
[296]1319
[236]1320                for part in message_parts:
1321                       
[296]1322                        ## If not an attachment, leave it alone
1323                        #
[236]1324                        if not isinstance(part, tuple):
1325                                renamed_parts.append(part)
1326                                continue
1327                               
1328                        (filename, part) = part
[295]1329
[296]1330                        ## If no filename, use a default one
1331                        #
1332                        if not filename:
[236]1333                                filename = 'untitled-part'
[22]1334
[242]1335                                # Guess the extension from the content type, use non strict mode
1336                                # some additional non-standard but commonly used MIME types
1337                                # are also recognized
1338                                #
1339                                ext = mimetypes.guess_extension(part.get_content_type(), False)
[236]1340                                if not ext:
1341                                        ext = '.bin'
[22]1342
[236]1343                                filename = '%s%s' % (filename, ext)
[22]1344
[296]1345# We now use the attachment insert function
1346#
1347                        ## Discard relative paths in attachment names
1348                        #
1349                        #filename = filename.replace('\\', '/').replace(':', '/')
1350                        #filename = os.path.basename(filename)
1351                        #
[236]1352                        # We try to normalize the filename to utf-8 NFC if we can.
1353                        # Files uploaded from OS X might be in NFD.
1354                        # Check python version and then try it
1355                        #
[296]1356                        #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
1357                        #       try:
1358                        #               filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
1359                        #       except TypeError:
1360                        #               pass
[100]1361
[236]1362                        # Make the filename unique for this ticket
1363                        num = 0
1364                        unique_filename = filename
[296]1365                        dummy_filename, ext = os.path.splitext(filename)
[134]1366
[253]1367                        while unique_filename in attachment_names or self.attachment_exists(unique_filename):
[236]1368                                num += 1
[296]1369                                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
[236]1370                               
1371                        if self.DEBUG:
1372                                print 'TD: Attachment with filename %s will be saved as %s' % (filename, unique_filename)
[100]1373
[236]1374                        attachment_names.add(unique_filename)
1375
1376                        renamed_parts.append((filename, unique_filename, part))
[296]1377       
[236]1378                return renamed_parts
1379                       
1380        def inline_part(self, part):
1381                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1382               
1383                       
[253]1384        def attachment_exists(self, filename):
[250]1385
1386                if self.DEBUG:
[253]1387                        print "TD: attachment_exists: Ticket number : %s, Filename : %s" %(self.id, filename)
[250]1388
1389                # We have no valid ticket id
1390                #
[253]1391                if not self.id:
[236]1392                        return False
[250]1393
[236]1394                try:
[253]1395                        att = attachment.Attachment(self.env, 'ticket', self.id, filename)
[236]1396                        return True
[250]1397                except attachment.ResourceNotFound:
[236]1398                        return False
1399                       
1400        def body_text(self, message_parts):
1401                body_text = []
1402               
1403                for part in message_parts:
1404                        # Plain text part, append it
1405                        if not isinstance(part, tuple):
1406                                body_text.extend(part.strip().splitlines())
1407                                body_text.append("")
1408                                continue
1409                               
1410                        (original, filename, part) = part
1411                        inline = self.inline_part(part)
1412                       
1413                        if part.get_content_maintype() == 'image' and inline:
1414                                body_text.append('[[Image(%s)]]' % filename)
1415                                body_text.append("")
1416                        else:
1417                                body_text.append('[attachment:"%s"]' % filename)
1418                                body_text.append("")
1419                               
1420                body_text = '\r\n'.join(body_text)
1421                return body_text
1422
[253]1423        def notify(self, tkt, new=True, modtime=0):
[79]1424                """
1425                A wrapper for the TRAC notify function. So we can use templates
1426                """
[250]1427                if self.DRY_RUN:
1428                                print 'DRY_RUN: self.notify(tkt, True) ', self.author
1429                                return
[41]1430                try:
1431                        # create false {abs_}href properties, to trick Notify()
1432                        #
[193]1433                        if not self.VERSION == 0.11:
[192]1434                                self.env.abs_href = Href(self.get_config('project', 'url'))
1435                                self.env.href = Href(self.get_config('project', 'url'))
[22]1436
[41]1437                        tn = TicketNotifyEmail(self.env)
[213]1438
[42]1439                        if self.notify_template:
[222]1440
[221]1441                                if self.VERSION == 0.11:
[222]1442
[221]1443                                        from trac.web.chrome import Chrome
[222]1444
1445                                        if self.notify_template_update and not new:
1446                                                tn.template_name = self.notify_template_update
1447                                        else:
1448                                                tn.template_name = self.notify_template
1449
[221]1450                                        tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
1451                                               
1452                                else:
[222]1453
[221]1454                                        tn.template_name = self.notify_template;
[42]1455
[77]1456                        tn.notify(tkt, new, modtime)
[41]1457
1458                except Exception, e:
[253]1459                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(self.id, e)
[41]1460
[253]1461        def html_mailto_link(self, subject, body):
1462                """
1463                This function returns a HTML mailto tag with the ticket id and author email address
1464                """
[72]1465                if not self.author:
[143]1466                        author = self.email_addr
[22]1467                else:   
[142]1468                        author = self.author
[22]1469
[253]1470                # use urllib to escape the chars
[22]1471                #
[288]1472                s = 'mailto:%s?Subject=%s&Cc=%s' %(
[74]1473                       urllib.quote(self.email_addr),
[253]1474                           urllib.quote('Re: #%s: %s' %(self.id, subject)),
[74]1475                           urllib.quote(self.MAILTO_CC)
1476                           )
1477
[290]1478                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)
[288]1479                return s
[22]1480
[253]1481        def attachments(self, message_parts, update=False):
[79]1482                '''
1483                save any attachments as files in the ticket's directory
1484                '''
[237]1485                if self.DRY_RUN:
[250]1486                        print "DRY_RUN: no attachments saved"
[237]1487                        return ''
1488
[22]1489                count = 0
[152]1490
1491                # Get Maxium attachment size
1492                #
1493                max_size = int(self.get_config('attachment', 'max_size'))
[153]1494                status   = ''
[236]1495               
1496                for part in message_parts:
1497                        # Skip body parts
1498                        if not isinstance(part, tuple):
[22]1499                                continue
[236]1500                               
1501                        (original, filename, part) = part
[48]1502                        #
[172]1503                        # Must be tuneables HvB
1504                        #
[236]1505                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, filename))
[22]1506                        text = part.get_payload(decode=1)
1507                        if not text:
1508                                text = '(None)'
[48]1509                        fd.write(text)
1510                        fd.close()
[22]1511
[153]1512                        # get the file_size
[22]1513                        #
[48]1514                        stats = os.lstat(path)
[153]1515                        file_size = stats[stat.ST_SIZE]
[22]1516
[152]1517                        # Check if the attachment size is allowed
1518                        #
[153]1519                        if (max_size != -1) and (file_size > max_size):
1520                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
[236]1521                                        %(status, original, file_size, max_size)
[152]1522
1523                                os.unlink(path)
1524                                continue
1525                        else:
1526                                count = count + 1
1527                                       
[172]1528                        # Insert the attachment
[73]1529                        #
[242]1530                        fd = open(path, 'rb')
[253]1531                        att = attachment.Attachment(self.env, 'ticket', self.id)
[73]1532
[172]1533                        # This will break the ticket_update system, the body_text is vaporized
1534                        # ;-(
1535                        #
1536                        if not update:
1537                                att.author = self.author
1538                                att.description = self.email_to_unicode('Added by email2trac')
[73]1539
[236]1540                        att.insert(filename, fd, file_size)
[296]1541
[172]1542                        #except  util.TracError, detail:
1543                        #       print detail
[73]1544
[103]1545                        # Remove the created temporary filename
1546                        #
[172]1547                        fd.close()
[103]1548                        os.unlink(path)
1549
[77]1550                # Return how many attachments
1551                #
[153]1552                status = 'This message has %d attachment(s)\n%s' %(count, status)
1553                return status
[22]1554
[77]1555
[22]1556def mkdir_p(dir, mode):
1557        '''do a mkdir -p'''
1558
1559        arr = string.split(dir, '/')
1560        path = ''
1561        for part in arr:
1562                path = '%s/%s' % (path, part)
1563                try:
1564                        stats = os.stat(path)
1565                except OSError:
1566                        os.mkdir(path, mode)
1567
1568def ReadConfig(file, name):
1569        """
1570        Parse the config file
1571        """
1572        if not os.path.isfile(file):
[79]1573                print 'File %s does not exist' %file
[22]1574                sys.exit(1)
1575
[199]1576        config = trac_config.Configuration(file)
[22]1577
1578        # Use given project name else use defaults
1579        #
1580        if name:
[199]1581                sections = config.sections()
1582                if not name in sections:
[79]1583                        print "Not a valid project name: %s" %name
[199]1584                        print "Valid names: %s" %sections
[22]1585                        sys.exit(1)
1586
1587                project =  dict()
[199]1588                for option, value in  config.options(name):
1589                        project[option] = value
[22]1590
1591        else:
[270]1592                # use some trac internals to get the defaults
[217]1593                #
1594                project = config.parser.defaults()
[22]1595
1596        return project
1597
[87]1598
[22]1599if __name__ == '__main__':
1600        # Default config file
1601        #
[24]1602        configfile = '@email2trac_conf@'
[22]1603        project = ''
1604        component = ''
[202]1605        ticket_prefix = 'default'
[204]1606        dry_run = None
[202]1607
[87]1608        ENABLE_SYSLOG = 0
[201]1609
[204]1610
[202]1611        SHORT_OPT = 'chf:np:t:'
1612        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=']
[201]1613
[22]1614        try:
[201]1615                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
[22]1616        except getopt.error,detail:
1617                print __doc__
1618                print detail
1619                sys.exit(1)
[87]1620       
[22]1621        project_name = None
1622        for opt,value in opts:
1623                if opt in [ '-h', '--help']:
1624                        print __doc__
1625                        sys.exit(0)
1626                elif opt in ['-c', '--component']:
1627                        component = value
1628                elif opt in ['-f', '--file']:
1629                        configfile = value
[201]1630                elif opt in ['-n', '--dry-run']:
[204]1631                        dry_run = True
[22]1632                elif opt in ['-p', '--project']:
1633                        project_name = value
[202]1634                elif opt in ['-t', '--ticket_prefix']:
1635                        ticket_prefix = value
[87]1636       
[22]1637        settings = ReadConfig(configfile, project_name)
1638        if not settings.has_key('project'):
1639                print __doc__
[79]1640                print 'No Trac project is defined in the email2trac config file.'
[22]1641                sys.exit(1)
[87]1642       
[22]1643        if component:
1644                settings['component'] = component
[202]1645
1646        # The default prefix for ticket values in email2trac.conf
1647        #
1648        settings['ticket_prefix'] = ticket_prefix
[206]1649        settings['dry_run'] = dry_run
[87]1650       
[22]1651        if settings.has_key('trac_version'):
[189]1652                version = settings['trac_version']
[22]1653        else:
1654                version = trac_default_version
1655
[189]1656
[22]1657        #debug HvB
1658        #print settings
[189]1659
[87]1660        try:
[189]1661                if version == '0.9':
[87]1662                        from trac import attachment
1663                        from trac.env import Environment
1664                        from trac.ticket import Ticket
1665                        from trac.web.href import Href
1666                        from trac import util
1667                        from trac.Notify import TicketNotifyEmail
[189]1668                elif version == '0.10':
[87]1669                        from trac import attachment
1670                        from trac.env import Environment
1671                        from trac.ticket import Ticket
1672                        from trac.web.href import Href
1673                        from trac import util
[139]1674                        #
1675                        # return  util.text.to_unicode(str)
1676                        #
[87]1677                        # see http://projects.edgewall.com/trac/changeset/2799
1678                        from trac.ticket.notification import TicketNotifyEmail
[199]1679                        from trac import config as trac_config
[189]1680                elif version == '0.11':
[182]1681                        from trac import attachment
1682                        from trac.env import Environment
1683                        from trac.ticket import Ticket
1684                        from trac.web.href import Href
[199]1685                        from trac import config as trac_config
[182]1686                        from trac import util
[260]1687
1688
[182]1689                        #
1690                        # return  util.text.to_unicode(str)
1691                        #
1692                        # see http://projects.edgewall.com/trac/changeset/2799
1693                        from trac.ticket.notification import TicketNotifyEmail
[189]1694                else:
1695                        print 'TRAC version %s is not supported' %version
1696                        sys.exit(1)
1697                       
1698                if settings.has_key('enable_syslog'):
[190]1699                        if SYSLOG_AVAILABLE:
1700                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
[182]1701
[291]1702
1703                # Must be set before environment is created
1704                #
1705                if settings.has_key('python_egg_cache'):
1706                        python_egg_cache = str(settings['python_egg_cache'])
1707                        os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
1708
[87]1709                env = Environment(settings['project'], create=0)
[206]1710                tktparser = TicketEmailParser(env, settings, float(version))
[87]1711                tktparser.parse(sys.stdin)
[22]1712
[87]1713        # Catch all errors ans log to SYSLOG if we have enabled this
1714        # else stdout
1715        #
1716        except Exception, error:
1717                if ENABLE_SYSLOG:
1718                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
[187]1719
[87]1720                        etype, evalue, etb = sys.exc_info()
1721                        for e in traceback.format_exception(etype, evalue, etb):
1722                                syslog.syslog(e)
[187]1723
[87]1724                        syslog.closelog()
1725                else:
1726                        traceback.print_exc()
[22]1727
[97]1728                if m:
[98]1729                        tktparser.save_email_for_debug(m, True)
[97]1730
[249]1731                sys.exit(1)
[22]1732# EOB
Note: See TracBrowser for help on using the repository browser.