source: trunk/email2trac.py.in @ 353

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

email2trac.py.in:

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