source: trunk/email2trac.py.in @ 177

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

email2trac.py.in:

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