source: trunk/email2trac.py.in @ 173

Last change on this file since 173 was 173, checked in by bas, 15 years ago

email2trac.py.in, email2trac.conf:

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