source: trunk/email2trac.py.in @ 194

Last change on this file since 194 was 194, checked in by bas, 16 years ago

email2trac.py.in, email2trac.conf:

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 28.1 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
30Changed By: Bas van der Vlies <basv@sara.nl>
31Date      : 13 September 2005
32Descr.    : Added config file and command line options, spam level
33            detection, reply address and mailto option. Unicode support
34
35Changed By: Walter de Jong <walter@sara.nl>
36Descr.    : multipart-message code and trac attachments
37
38
39The scripts reads emails from stdin and inserts directly into a Trac database.
40MIME headers are mapped as follows:
41
42        * From:      => Reporter
[152]43                     => CC (Optional via reply_all option)
[22]44        * Subject:   => Summary
45        * Body       => Description
46        * Component  => Can be set to SPAM via spam_level option
47
48How to use
49----------
50 * Create an config file:
[74]51        [DEFAULT]                      # REQUIRED
52        project      : /data/trac/test # REQUIRED
53        debug        : 1               # OPTIONAL, if set print some DEBUG info
54        spam_level   : 4               # OPTIONAL, if set check for SPAM mail
[152]55        reply_all    : 1               # OPTIONAL, if set then fill in ticket CC field
[87]56        umask        : 022             # OPTIONAL, if set then use this umask for creation of the attachments
[74]57        mailto_link  : 1               # OPTIONAL, if set then [mailto:<>] in description
[75]58        mailto_cc    : basv@sara.nl    # OPTIONAL, use this address as CC in mailto line
[74]59        ticket_update: 1               # OPTIONAL, if set then check if this is an update for a ticket
[172]60        trac_version : 0.9             # OPTIONAL, default is 0.10
[22]61
[148]62        [jouvin]                       # OPTIONAL project declaration, if set both fields necessary
[22]63        project      : /data/trac/jouvin # use -p|--project jouvin. 
64       
65 * default config file is : /etc/email2trac.conf
66
67 * Commandline opions:
68                -h | --help
69                -c <value> | --component=<value>
70                -f <config file> | --file=<config file>
71                -p <project name> | --project=<project name>
72
73SVN Info:
74        $Id: email2trac.py.in 194 2008-02-04 10:27:22Z bas $
75"""
76import os
77import sys
78import string
79import getopt
80import stat
81import time
82import email
[136]83import email.Iterators
84import email.Header
[22]85import re
86import urllib
87import unicodedata
88import ConfigParser
89from stat import *
90import mimetypes
[96]91import traceback
[22]92
[190]93
94# Will fail where unavailable, e.g. Windows
95#
96try:
97    import syslog
98    SYSLOG_AVAILABLE = True
99except ImportError:
100    SYSLOG_AVAILABLE = False
101
[182]102from datetime import tzinfo, timedelta, datetime
[91]103
[182]104
[96]105# Some global variables
106#
[189]107trac_default_version = '0.10'
[96]108m = None
[22]109
[96]110
[182]111
112# A UTC class needed for trac version 0.11, added by
113# tbaschak at ktc dot mb dot ca
114#
115class UTC(tzinfo):
116        """UTC"""
117        ZERO = timedelta(0)
118        HOUR = timedelta(hours=1)
119       
120        def utcoffset(self, dt):
121                return self.ZERO
122               
123        def tzname(self, dt):
124                return "UTC"
125               
126        def dst(self, dt):
127                return self.ZERO
128
129
[22]130class TicketEmailParser(object):
131        env = None
132        comment = '> '
133   
134        def __init__(self, env, parameters, version):
135                self.env = env
136
137                # Database connection
138                #
139                self.db = None
140
[72]141                # Some useful mail constants
142                #
143                self.author = None
144                self.email_addr = None
[183]145                self.email_from = None
[72]146
[22]147                self.VERSION = version
[172]148                self.get_config = self.env.config.get
[22]149
150                if parameters.has_key('umask'):
151                        os.umask(int(parameters['umask'], 8))
152
153                if parameters.has_key('debug'):
154                        self.DEBUG = int(parameters['debug'])
155                else:
156                        self.DEBUG = 0
157
158                if parameters.has_key('mailto_link'):
159                        self.MAILTO = int(parameters['mailto_link'])
[74]160                        if parameters.has_key('mailto_cc'):
161                                self.MAILTO_CC = parameters['mailto_cc']
162                        else:
163                                self.MAILTO_CC = ''
[22]164                else:
165                        self.MAILTO = 0
166
167                if parameters.has_key('spam_level'):
168                        self.SPAM_LEVEL = int(parameters['spam_level'])
169                else:
170                        self.SPAM_LEVEL = 0
171
[191]172                if parameters.has_key('email_quote'):
173                        self.EMAIL_QUOTE = str(parameters['email_quote'])
174                else:   
175                        self.EMAIL_QUOTE = '> '
[22]176
177                if parameters.has_key('email_header'):
178                        self.EMAIL_HEADER = int(parameters['email_header'])
179                else:
180                        self.EMAIL_HEADER = 0
181
[42]182                if parameters.has_key('alternate_notify_template'):
183                        self.notify_template = str(parameters['alternate_notify_template'])
184                else:
185                        self.notify_template = None
[22]186
[43]187                if parameters.has_key('reply_all'):
188                        self.REPLY_ALL = int(parameters['reply_all'])
189                else:
190                        self.REPLY_ALL = 0
[42]191
[74]192                if parameters.has_key('ticket_update'):
193                        self.TICKET_UPDATE = int(parameters['ticket_update'])
194                else:
195                        self.TICKET_UPDATE = 0
[43]196
[118]197                if parameters.has_key('drop_spam'):
198                        self.DROP_SPAM = int(parameters['drop_spam'])
199                else:
200                        self.DROP_SPAM = 0
[74]201
[134]202                if parameters.has_key('verbatim_format'):
203                        self.VERBATIM_FORMAT = int(parameters['verbatim_format'])
204                else:
205                        self.VERBATIM_FORMAT = 1
[118]206
[136]207                if parameters.has_key('strip_signature'):
208                        self.STRIP_SIGNATURE = int(parameters['strip_signature'])
209                else:
210                        self.STRIP_SIGNATURE = 0
[134]211
[191]212                if parameters.has_key('strip_quotes'):
213                        self.STRIP_QUOTES = int(parameters['strip_quotes'])
214                else:
215                        self.STRIP_QUOTES = 0
216
[148]217                if parameters.has_key('use_textwrap'):
218                        self.USE_TEXTWRAP = int(parameters['use_textwrap'])
219                else:
220                        self.USE_TEXTWRAP = 0
221
[163]222                if parameters.has_key('python_egg_cache'):
223                        self.python_egg_cache = str(parameters['python_egg_cache'])
224                        os.environ['PYTHON_EGG_CACHE'] = self.python_egg_cache
225
[173]226                # Use OS independend functions
227                #
228                self.TMPDIR = os.path.normcase('/tmp')
229                if parameters.has_key('tmpdir'):
230                        self.TMPDIR = os.path.normcase(str(parameters['tmpdir']))
231
[194]232                if parameters.has_key('ignore_trac_user_settings'):
233                        self.IGNORE_TRAC_USER_SETTINGS = int(parameters['ignore_trac_user_settings'])
234                else:
235                        self.IGNORE_TRAC_USER_SETTINGS = 0
[191]236
[22]237        def spam(self, message):
[191]238                """
239                # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
240                # Note if Spam_level then '*' are included
241                """
[194]242                spam = False
[22]243                if message.has_key('X-Spam-Score'):
244                        spam_l = string.split(message['X-Spam-Score'])
245                        number = spam_l[0].count('*')
246
247                        if number >= self.SPAM_LEVEL:
[194]248                                spam = True
249                               
[191]250                # treat virus mails as spam
251                #
252                elif message.has_key('X-Virus-found'):                 
[194]253                        spam = True
254
255                # How to handle SPAM messages
256                #
257                if self.DROP_SPAM and spam:
258                        if self.DEBUG > 2 :
259                                print 'This message is a SPAM. Automatic ticket insertion refused (SPAM level > %d' % self.SPAM_LEVEL
260
261                        return True     
262
263                elif spam:
264
[67]265                        return 'Spam'
266
[194]267                else:
[22]268
[194]269                        return self.get_config('ticket', 'default_component')
[191]270
[194]271
[149]272        def blacklisted_from(self):
273                FROM_RE = re.compile(r"""
274                    MAILER-DAEMON@
[194]275                    """, re.VERBOSE|re.IGNORECASE)
[149]276                result =  FROM_RE.search(self.email_addr)
277                if result:
278                        return True
279                else:
280                        return False
281
[139]282        def email_to_unicode(self, message_str):
[22]283                """
284                Email has 7 bit ASCII code, convert it to unicode with the charset
[79]285        that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
[22]286                understands it.
287                """
[139]288                results =  email.Header.decode_header(message_str)
[22]289                str = None
290                for text,format in results:
291                        if format:
292                                try:
293                                        temp = unicode(text, format)
[139]294                                except UnicodeError, detail:
[22]295                                        # This always works
296                                        #
297                                        temp = unicode(text, 'iso-8859-15')
[139]298                                except LookupError, detail:
299                                        #text = 'ERROR: Could not find charset: %s, please install' %format
300                                        #temp = unicode(text, 'iso-8859-15')
301                                        temp = message_str
302                                       
[22]303                        else:
304                                temp = string.strip(text)
[92]305                                temp = unicode(text, 'iso-8859-15')
[22]306
307                        if str:
[100]308                                str = '%s %s' %(str, temp)
[22]309                        else:
[100]310                                str = '%s' %temp
[22]311
[139]312                #str = str.encode('utf-8')
[22]313                return str
314
315        def debug_attachments(self, message):
316                n = 0
317                for part in message.walk():
318                        if part.get_content_maintype() == 'multipart':      # multipart/* is just a container
319                                print 'TD: multipart container'
320                                continue
321
322                        n = n + 1
323                        print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type())
324                        print 'TD: part%d: filename: %s' % (n, part.get_filename())
325
326                        if part.is_multipart():
327                                print 'TD: this part is multipart'
328                                payload = part.get_payload(decode=1)
329                                print 'TD: payload:', payload
330                        else:
331                                print 'TD: this part is not multipart'
332
[173]333                        file = 'part%d' %n
334                        part_file = os.path.join(self.TMPDIR, file)
335                        #part_file = '/var/tmp/part%d' % n
[22]336                        print 'TD: writing part%d (%s)' % (n,part_file)
337                        fx = open(part_file, 'wb')
338                        text = part.get_payload(decode=1)
339                        if not text:
340                                text = '(None)'
341                        fx.write(text)
342                        fx.close()
343                        try:
344                                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
345                        except OSError:
346                                pass
347
348        def email_header_txt(self, m):
[72]349                """
350                Display To and CC addresses in description field
351                """
[22]352                str = ''
353                if m['To'] and len(m['To']) > 0 and m['To'] != 'hic@sara.nl':
354                        str = "'''To:''' %s [[BR]]" %(m['To'])
355                if m['Cc'] and len(m['Cc']) > 0:
356                        str = "%s'''Cc:''' %s [[BR]]" % (str, m['Cc'])
357
[139]358                return  self.email_to_unicode(str)
[22]359
[138]360
[43]361        def set_owner(self, ticket):
[45]362                """
363                Select default owner for ticket component
364                """
[176]365                #### return self.get_config('ticket', 'default_component')
[43]366                cursor = self.db.cursor()
367                sql = "SELECT owner FROM component WHERE name='%s'" % ticket['component']
368                cursor.execute(sql)
[61]369                try:
370                        ticket['owner'] = cursor.fetchone()[0]
371                except TypeError, detail:
[176]372                        ticket['owner'] = None
[43]373
[194]374        def get_sender_info(self, message):
[45]375                """
[72]376                Get the default author name and email address from the message
[45]377                """
[43]378
[194]379                self.email_from = self.email_to_unicode(message['from'])
380                self.author, self.email_addr  = email.Utils.parseaddr(self.email_from)
[142]381
[194]382                # Maybe for later user
383                #self.email_from =  self.email_to_unicode(self.email_addr)
384
385
386                if self.IGNORE_TRAC_USER_SETTINGS:
387                        return
388
389                # Is this a registered user, use email address as search key:
390                # result:
391                #   u : login name
392                #   n : Name that the user has set in the settings tab
393                #   e : email address that the user has set in the settings tab
[45]394                #
[194]395                users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
[72]396                                if e == self.email_addr ]
[43]397
[45]398                if len(users) == 1:
[194]399                        self.email_from = 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                """
[183]405                ticket['reporter'] = self.email_from
[72]406
[45]407                # Put all CC-addresses in ticket CC field
[43]408                #
409                if self.REPLY_ALL:
[45]410                        #tos = message.get_all('to', [])
[43]411                        ccs = message.get_all('cc', [])
412
[45]413                        addrs = email.Utils.getaddresses(ccs)
[105]414                        if not addrs:
415                                return
[43]416
417                        # Remove reporter email address if notification is
418                        # on
419                        #
420                        if self.notification:
421                                try:
[72]422                                        addrs.remove((self.author, self.email_addr))
[43]423                                except ValueError, detail:
424                                        pass
425
[45]426                        for name,mail in addrs:
[105]427                                try:
[108]428                                        mail_list = '%s, %s' %(mail_list, mail)
[105]429                                except:
430                                        mail_list = mail
[43]431
[105]432                        if mail_list:
[139]433                                ticket['cc'] = self.email_to_unicode(mail_list)
[96]434
435        def save_email_for_debug(self, message, tempfile=False):
436                if tempfile:
437                        import tempfile
438                        msg_file = tempfile.mktemp('.email2trac')
439                else:
[173]440                        #msg_file = '/var/tmp/msg.txt'
441                        msg_file = os.path.join(self.TMPDIR, 'msg.txt')
442
[44]443                print 'TD: saving email to %s' % msg_file
444                fx = open(msg_file, 'wb')
445                fx.write('%s' % message)
446                fx.close()
447                try:
448                        os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
449                except OSError:
450                        pass
451
[167]452        def str_to_dict(self, str):
[164]453                """
454                Transfrom a str of the form [<key>=<value>]+ to dict[<key>] = <value>
455                """
456                # Skip the last ':' character
457                #
458                fields = string.split(str[:-1], ',')
459
460                result = dict()
461                for field in fields:
462                        try:
463                                index, value = string.split(field,'=')
[169]464
465                                # We can not change the description of a ticket via the subject
466                                # line. The description is the body of the email
467                                #
468                                if index.lower() in ['description']:
469                                        continue
470
[164]471                                if value:
[165]472                                        result[index.lower()] = value
[169]473
[164]474                        except ValueError:
475                                pass
476
[165]477                return result
[167]478
[169]479        def update_ticket_fields(self, ticket, user_dict):
480                """
481                This will update the ticket fields when supplied via
482                the subject mail line. It will only update the ticket
483                field:
484                        - If the field is known
485                        - If the value supplied is valid for the ticket field
486                        - Else we skip it and no error is given
487                """
488
489                # Build a system dictionary from the ticket fields
490                # with field as index and option as value
491                #
492                sys_dict = dict()
493                for field in ticket.fields:
[167]494                        try:
[169]495                                sys_dict[field['name']] = field['options']
496
[167]497                        except KeyError:
[169]498                                sys_dict[field['name']] = None
[167]499                                pass
[169]500
501                # Check user supplied fields an compare them with the
502                # system one's
503                #
504                for field,value in user_dict.items():
505                        if self.DEBUG >= 5:
506                                print  'user field : %s=%s' %(field,value)
507
508                        if sys_dict.has_key(field):
509                                if self.DEBUG >= 5:
510                                        print  'sys field  : ', sys_dict[field]
511
512                                # Check if value is an allowed system option, if TypeError then
513                                # every value is allowed
514                                #
515                                try:
516                                        if value in sys_dict[field]:
517                                                ticket[field] = value
518
519                                except TypeError:
520                                        ticket[field] = value
521                                       
[164]522                               
[169]523                               
[71]524        def ticket_update(self, m):
[78]525                """
[79]526                If the current email is a reply to an existing ticket, this function
527                will append the contents of this email to that ticket, instead of
528                creating a new one.
[78]529                """
[71]530                if not m['Subject']:
531                        return False
532                else:
[139]533                        subject  = self.email_to_unicode(m['Subject'])
[71]534
[182]535                # [hic] #1529: Re: LRZ
536                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
537                #
[71]538                TICKET_RE = re.compile(r"""
539                                        (?P<ticketnr>[#][0-9]+:)
[194]540                                        |(?P<ticketnr_fields>[#][\d]+\?.*?:)
[71]541                                        """, re.VERBOSE)
542
543                result =  TICKET_RE.search(subject)
544                if not result:
545                        return False
546
[164]547                # Must we update ticket fields
548                #
[167]549                update_tkt_fields = dict()
[165]550                try:
[164]551                        nr, keywords = string.split(result.group('ticketnr_fields'), '?')
[167]552                        update_tkt_fields = self.str_to_dict(keywords)
[165]553
554                        # Strip '#'
555                        #
556                        ticket_id = int(nr[1:])
557
558                except AttributeError:
559                        # Strip '#' and ':'
560                        #
[167]561                        nr = result.group('ticketnr')
[165]562                        ticket_id = int(nr[1:-1])
[164]563
[71]564
[194]565                # When is the change committed
566                #
[77]567                #
[194]568                if self.VERSION == 0.11:
569                        utc = UTC()
570                        when = datetime.now(utc)
571                else:
572                        when = int(time.time())
[77]573
[172]574                try:
575                        tkt = Ticket(self.env, ticket_id, self.db)
576                except util.TracError, detail:
577                        return False
[126]578
[172]579                # Must we update some ticket fields properties
580                #
581                if update_tkt_fields:
[169]582                        self.update_ticket_fields(tkt, update_tkt_fields)
[166]583
[172]584                body_text = self.get_body_text(m)
[177]585                if self.EMAIL_HEADER:
586                        head = self.email_header_txt(m)
587                        body_text = u"\r\n%s \r\n%s" %(head, body_text)
[76]588
[177]589                #if self.MAILTO:
590                #       mailto = self.html_mailto_link(tkt['summary'], ticket_id, body_text)
591                #       body_text = u"\r\n%s \r\n%s" %(mailto, body_text)
592
[172]593                tkt.save_changes(self.author, body_text, when)
594                tkt['id'] = ticket_id
595
[129]596                if self.VERSION  == 0.9:
[152]597                        str = self.attachments(m, tkt, True)
[129]598                else:
[152]599                        str = self.attachments(m, tkt)
[76]600
[72]601                if self.notification:
[77]602                        self.notify(tkt, False, when)
[72]603
[71]604                return True
605
[194]606        def new_ticket(self, msg, component):
[77]607                """
608                Create a new ticket
609                """
[41]610                tkt = Ticket(self.env)
[22]611                tkt['status'] = 'new'
612
613                # Some defaults
614                #
615                tkt['milestone'] = self.get_config('ticket', 'default_milestone')
616                tkt['priority'] = self.get_config('ticket', 'default_priority')
617                tkt['severity'] = self.get_config('ticket', 'default_severity')
618                tkt['version'] = self.get_config('ticket', 'default_version')
619
620                if not msg['Subject']:
[151]621                        tkt['summary'] = u'(No subject)'
[22]622                else:
[139]623                        tkt['summary'] = self.email_to_unicode(msg['Subject'])
[22]624
[41]625                if settings.has_key('component'):
[22]626                        tkt['component'] = settings['component']
627                else:
[194]628                        tkt['component'] = component
[22]629
[176]630                # Set default owner for component, HvB
631                # Is not necessary, because if component is set. The trac code
632                # will find the owner: self.set_owner(tkt)
[22]633                #
[72]634                self.set_reply_fields(tkt, msg)
[22]635
[45]636                # produce e-mail like header
637                #
[22]638                head = ''
639                if self.EMAIL_HEADER > 0:
640                        head = self.email_header_txt(msg)
[92]641                       
[72]642                body_text = self.get_body_text(msg)
[45]643
[140]644                tkt['description'] = '\r\n%s\r\n%s' \
[142]645                        %(head, body_text)
[90]646
[182]647                #when = int(time.time())
[192]648                #
[182]649                utc = UTC()
650                when = datetime.now(utc)
[45]651
[172]652                ticket_id = tkt.insert()
[187]653                #try:
654                #       ticket_id = tkt.insert()
655                #except OperationalError, detail:
656                #       syslog.openlog('email2trac', syslog.LOG_NOWAIT)
657                #       syslog.syslog('catch tkt insert problem %s' %detail)
658                #       syslog.closelog()
659                #
660                #       ticket_id = tkt.insert()
661                       
[172]662                tkt['id'] = ticket_id
663
[90]664                changed = False
665                comment = ''
[77]666
[90]667                # Rewrite the description if we have mailto enabled
[45]668                #
[72]669                if self.MAILTO:
[100]670                        changed = True
[142]671                        comment = u'\nadded mailto line\n'
672                        mailto = self.html_mailto_link(tkt['summary'], ticket_id, body_text)
673                        tkt['description'] = u'\r\n%s\r\n%s%s\r\n' \
674                                %(head, mailto, body_text)
[45]675
[152]676                str =  self.attachments(msg, tkt)
677                if str:
[100]678                        changed = True
[152]679                        comment = '%s\n%s\n' %(comment, str)
[77]680
[90]681                if changed:
[172]682                        tkt.save_changes(self.author, comment)
683                        #print tkt.get_changelog(self.db, when)
[90]684
[45]685                if self.notification:
[90]686                        self.notify(tkt, True)
687                        #self.notify(tkt, False)
[45]688
[77]689        def parse(self, fp):
[96]690                global m
691
[77]692                m = email.message_from_file(fp)
693                if not m:
694                        return
695
696                if self.DEBUG > 1:        # save the entire e-mail message text
697                        self.save_email_for_debug(m)
698                        self.debug_attachments(m)
699
700                self.db = self.env.get_db_cnx()
[194]701                self.get_sender_info(m)
[152]702
[149]703                if self.blacklisted_from():
704                        if self.DEBUG > 1 :
705                                print 'Message rejected : From: in blacklist'
706                        return False
[77]707
[194]708            # If component is true then we drop the message drop message
709                #
710                component =  self.spam(m)
711                if component == True:
712                        return False
713
714
[77]715                if self.get_config('notification', 'smtp_enabled') in ['true']:
716                        self.notification = 1
717                else:
718                        self.notification = 0
719
720                # Must we update existing tickets
721                #
722                if self.TICKET_UPDATE > 0:
723                        if self.ticket_update(m):
724                                return True
725
[194]726                self.new_ticket(m, component)
[77]727
[136]728        def strip_signature(self, text):
729                """
730                Strip signature from message, inspired by Mailman software
731                """
732                body = []
733                for line in text.splitlines():
734                        if line == '-- ':
735                                break
736                        body.append(line)
737
738                return ('\n'.join(body))
739
[191]740        def strip_quotes(self, text):
[193]741                """
742                Strip quotes from message by Nicolas Mendoza
743                """
744                body = []
745                for line in text.splitlines():
746                        if line.startswith(self.EMAIL_QUOTE):
747                                continue
748                        body.append(line)
[151]749
[193]750                return ('\n'.join(body))
[191]751
[154]752        def wrap_text(self, text, replace_whitespace = False):
[151]753                """
[191]754                Will break a lines longer then given length into several small
755                lines of size given length
[151]756                """
757                import textwrap
[154]758
[151]759                LINESEPARATOR = '\n'
[153]760                reformat = ''
[151]761
[154]762                for s in text.split(LINESEPARATOR):
763                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
764                        if tmp:
765                                reformat = '%s\n%s' %(reformat,tmp)
766                        else:
767                                reformat = '%s\n' %reformat
[153]768
769                return reformat
770
[154]771                # Python2.4 and higher
772                #
773                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
774                #
775
776
[72]777        def get_body_text(self, msg):
[45]778                """
[79]779                put the message text in the ticket description or in the changes field.
[45]780                message text can be plain text or html or something else
781                """
[22]782                has_description = 0
[100]783                encoding = True
[109]784                ubody_text = u'No plain text message'
[22]785                for part in msg.walk():
[45]786
787                        # 'multipart/*' is a container for multipart messages
788                        #
789                        if part.get_content_maintype() == 'multipart':
[22]790                                continue
791
792                        if part.get_content_type() == 'text/plain':
[45]793                                # Try to decode, if fails then do not decode
794                                #
[90]795                                body_text = part.get_payload(decode=1)
[45]796                                if not body_text:                       
[90]797                                        body_text = part.get_payload(decode=0)
[154]798       
[136]799                                if self.STRIP_SIGNATURE:
800                                        body_text = self.strip_signature(body_text)
[22]801
[191]802                                if self.STRIP_QUOTES:
803                                        body_text = self.strip_quotes(body_text)
804
[148]805                                if self.USE_TEXTWRAP:
[151]806                                        body_text = self.wrap_text(body_text)
[148]807
[45]808                                # Get contents charset (iso-8859-15 if not defined in mail headers)
809                                #
[100]810                                charset = part.get_content_charset()
[102]811                                if not charset:
812                                        charset = 'iso-8859-15'
813
[89]814                                try:
[96]815                                        ubody_text = unicode(body_text, charset)
[100]816
817                                except UnicodeError, detail:
[96]818                                        ubody_text = unicode(body_text, 'iso-8859-15')
[89]819
[100]820                                except LookupError, detail:
[139]821                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
[100]822
[22]823                        elif part.get_content_type() == 'text/html':
[109]824                                ubody_text = '(see attachment for HTML mail message)'
[22]825
826                        else:
[109]827                                ubody_text = '(see attachment for message)'
[22]828
829                        has_description = 1
830                        break           # we have the description, so break
831
832                if not has_description:
[109]833                        ubody_text = '(see attachment for message)'
[22]834
[100]835                # A patch so that the web-interface will not update the description
836                # field of a ticket
837                #
838                ubody_text = ('\r\n'.join(ubody_text.splitlines()))
[22]839
[100]840                #  If we can unicode it try to encode it for trac
841                #  else we a lot of garbage
842                #
[142]843                #if encoding:
844                #       ubody_text = ubody_text.encode('utf-8')
[100]845
[134]846                if self.VERBATIM_FORMAT:
847                        ubody_text = '{{{\r\n%s\r\n}}}' %ubody_text
848                else:
849                        ubody_text = '%s' %ubody_text
850
[100]851                return ubody_text
852
[77]853        def notify(self, tkt , new=True, modtime=0):
[79]854                """
855                A wrapper for the TRAC notify function. So we can use templates
856                """
[112]857                if tkt['component'] == 'Spam':
858                        return 
859
[41]860                try:
861                        # create false {abs_}href properties, to trick Notify()
862                        #
[193]863                        if not self.VERSION == 0.11:
[192]864                                self.env.abs_href = Href(self.get_config('project', 'url'))
865                                self.env.href = Href(self.get_config('project', 'url'))
[22]866
[41]867                        tn = TicketNotifyEmail(self.env)
[42]868                        if self.notify_template:
869                                tn.template_name = self.notify_template;
870
[77]871                        tn.notify(tkt, new, modtime)
[41]872
873                except Exception, e:
[79]874                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(tkt['id'], e)
[41]875
[72]876        def html_mailto_link(self, subject, id, body):
877                if not self.author:
[143]878                        author = self.email_addr
[22]879                else:   
[142]880                        author = self.author
[22]881
882                # Must find a fix
883                #
884                #arr = string.split(body, '\n')
885                #arr = map(self.mail_line, arr)
886                #body = string.join(arr, '\n')
887                #body = '%s wrote:\n%s' %(author, body)
888
889                # Temporary fix
[142]890                #
[74]891                str = 'mailto:%s?Subject=%s&Cc=%s' %(
892                       urllib.quote(self.email_addr),
893                           urllib.quote('Re: #%s: %s' %(id, subject)),
894                           urllib.quote(self.MAILTO_CC)
895                           )
896
[90]897                str = '\r\n{{{\r\n#!html\r\n<a href="%s">Reply to: %s</a>\r\n}}}\r\n' %(str, author)
[22]898                return str
899
[129]900        def attachments(self, message, ticket, update=False):
[79]901                '''
902                save any attachments as files in the ticket's directory
903                '''
[22]904                count = 0
[77]905                first = 0
906                number = 0
[152]907
908                # Get Maxium attachment size
909                #
910                max_size = int(self.get_config('attachment', 'max_size'))
[153]911                status   = ''
[152]912
[22]913                for part in message.walk():
914                        if part.get_content_maintype() == 'multipart':          # multipart/* is just a container
915                                continue
916
917                        if not first:                                                                           # first content is the message
918                                first = 1
919                                if part.get_content_type() == 'text/plain':             # if first is text, is was already put in the description
920                                        continue
921
922                        filename = part.get_filename()
923                        if not filename:
[77]924                                number = number + 1
925                                filename = 'part%04d' % number
[22]926
[72]927                                ext = mimetypes.guess_extension(part.get_content_type())
[22]928                                if not ext:
929                                        ext = '.bin'
930
931                                filename = '%s%s' % (filename, ext)
932                        else:
[139]933                                filename = self.email_to_unicode(filename)
[22]934
[48]935                        # From the trac code
936                        #
937                        filename = filename.replace('\\', '/').replace(':', '/')
938                        filename = os.path.basename(filename)
[22]939
[48]940                        # We try to normalize the filename to utf-8 NFC if we can.
941                        # Files uploaded from OS X might be in NFD.
[92]942                        # Check python version and then try it
[48]943                        #
944                        if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
[92]945                                try:
946                                        filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
947                                except TypeError:
948                                        pass
[48]949
[22]950                        url_filename = urllib.quote(filename)
[172]951                        #
952                        # Must be tuneables HvB
953                        #
[173]954                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, url_filename))
[22]955                        text = part.get_payload(decode=1)
956                        if not text:
957                                text = '(None)'
[48]958                        fd.write(text)
959                        fd.close()
[22]960
[153]961                        # get the file_size
[22]962                        #
[48]963                        stats = os.lstat(path)
[153]964                        file_size = stats[stat.ST_SIZE]
[22]965
[152]966                        # Check if the attachment size is allowed
967                        #
[153]968                        if (max_size != -1) and (file_size > max_size):
969                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
970                                        %(status, filename, file_size, max_size)
[152]971
972                                os.unlink(path)
973                                continue
974                        else:
975                                count = count + 1
976                                       
[172]977                        # Insert the attachment
[73]978                        #
[172]979                        fd = open(path)
980                        att = attachment.Attachment(self.env, 'ticket', ticket['id'])
[73]981
[172]982                        # This will break the ticket_update system, the body_text is vaporized
983                        # ;-(
984                        #
985                        if not update:
986                                att.author = self.author
987                                att.description = self.email_to_unicode('Added by email2trac')
[73]988
[172]989                        att.insert(url_filename, fd, file_size)
990                        #except  util.TracError, detail:
991                        #       print detail
[73]992
[103]993                        # Remove the created temporary filename
994                        #
[172]995                        fd.close()
[103]996                        os.unlink(path)
997
[77]998                # Return how many attachments
999                #
[153]1000                status = 'This message has %d attachment(s)\n%s' %(count, status)
1001                return status
[22]1002
[77]1003
[22]1004def mkdir_p(dir, mode):
1005        '''do a mkdir -p'''
1006
1007        arr = string.split(dir, '/')
1008        path = ''
1009        for part in arr:
1010                path = '%s/%s' % (path, part)
1011                try:
1012                        stats = os.stat(path)
1013                except OSError:
1014                        os.mkdir(path, mode)
1015
1016
1017def ReadConfig(file, name):
1018        """
1019        Parse the config file
1020        """
1021
1022        if not os.path.isfile(file):
[79]1023                print 'File %s does not exist' %file
[22]1024                sys.exit(1)
1025
1026        config = ConfigParser.ConfigParser()
1027        try:
1028                config.read(file)
1029        except ConfigParser.MissingSectionHeaderError,detail:
1030                print detail
1031                sys.exit(1)
1032
1033
1034        # Use given project name else use defaults
1035        #
1036        if name:
1037                if not config.has_section(name):
[79]1038                        print "Not a valid project name: %s" %name
[22]1039                        print "Valid names: %s" %config.sections()
1040                        sys.exit(1)
1041
1042                project =  dict()
1043                for option in  config.options(name):
1044                        project[option] = config.get(name, option)
1045
1046        else:
1047                project = config.defaults()
1048
1049        return project
1050
[87]1051
[22]1052if __name__ == '__main__':
1053        # Default config file
1054        #
[24]1055        configfile = '@email2trac_conf@'
[22]1056        project = ''
1057        component = ''
[87]1058        ENABLE_SYSLOG = 0
1059               
[22]1060        try:
1061                opts, args = getopt.getopt(sys.argv[1:], 'chf:p:', ['component=','help', 'file=', 'project='])
1062        except getopt.error,detail:
1063                print __doc__
1064                print detail
1065                sys.exit(1)
[87]1066       
[22]1067        project_name = None
1068        for opt,value in opts:
1069                if opt in [ '-h', '--help']:
1070                        print __doc__
1071                        sys.exit(0)
1072                elif opt in ['-c', '--component']:
1073                        component = value
1074                elif opt in ['-f', '--file']:
1075                        configfile = value
1076                elif opt in ['-p', '--project']:
1077                        project_name = value
[87]1078       
[22]1079        settings = ReadConfig(configfile, project_name)
1080        if not settings.has_key('project'):
1081                print __doc__
[79]1082                print 'No Trac project is defined in the email2trac config file.'
[22]1083                sys.exit(1)
[87]1084       
[22]1085        if component:
1086                settings['component'] = component
[87]1087       
[22]1088        if settings.has_key('trac_version'):
[189]1089                version = settings['trac_version']
[22]1090        else:
1091                version = trac_default_version
1092
[189]1093
[22]1094        #debug HvB
1095        #print settings
[189]1096
[87]1097        try:
[189]1098                if version == '0.9':
[87]1099                        from trac import attachment
1100                        from trac.env import Environment
1101                        from trac.ticket import Ticket
1102                        from trac.web.href import Href
1103                        from trac import util
1104                        from trac.Notify import TicketNotifyEmail
[189]1105                elif version == '0.10':
[87]1106                        from trac import attachment
1107                        from trac.env import Environment
1108                        from trac.ticket import Ticket
1109                        from trac.web.href import Href
1110                        from trac import util
[139]1111                        #
1112                        # return  util.text.to_unicode(str)
1113                        #
[87]1114                        # see http://projects.edgewall.com/trac/changeset/2799
1115                        from trac.ticket.notification import TicketNotifyEmail
[189]1116                elif version == '0.11':
[182]1117                        from trac import attachment
1118                        from trac.env import Environment
1119                        from trac.ticket import Ticket
1120                        from trac.web.href import Href
1121                        from trac import util
1122                        #
1123                        # return  util.text.to_unicode(str)
1124                        #
1125                        # see http://projects.edgewall.com/trac/changeset/2799
1126                        from trac.ticket.notification import TicketNotifyEmail
[189]1127                else:
1128                        print 'TRAC version %s is not supported' %version
1129                        sys.exit(1)
1130                       
1131                if settings.has_key('enable_syslog'):
[190]1132                        if SYSLOG_AVAILABLE:
1133                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
[182]1134
[87]1135                env = Environment(settings['project'], create=0)
[189]1136                tktparser = TicketEmailParser(env, settings, float(version))
[87]1137                tktparser.parse(sys.stdin)
[22]1138
[87]1139        # Catch all errors ans log to SYSLOG if we have enabled this
1140        # else stdout
1141        #
1142        except Exception, error:
1143                if ENABLE_SYSLOG:
1144                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
[187]1145
[87]1146                        etype, evalue, etb = sys.exc_info()
1147                        for e in traceback.format_exception(etype, evalue, etb):
1148                                syslog.syslog(e)
[187]1149
[87]1150                        syslog.closelog()
1151                else:
1152                        traceback.print_exc()
[22]1153
[97]1154                if m:
[98]1155                        tktparser.save_email_for_debug(m, True)
[97]1156
[22]1157# EOB
Note: See TracBrowser for help on using the repository browser.