source: trunk/email2trac.py.in @ 170

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

email2trac.py.in:

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