source: trunk/email2trac.py.in @ 195

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

email2trac.py.in, email2trac.conf:

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