source: trunk/email2trac.py.in @ 347

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

email2trac.py.in:

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