source: trunk/email2trac.py.in @ 187

Last change on this file since 187 was 187, checked in by bas, 17 years ago

email2trac.py.in:

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