source: trunk/email2trac.py.in @ 176

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

email2trac.py.in, ChangeLog?:

  • removed obsolete code set_owner
  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 25.6 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 176 2007-07-09 08:51:57Z 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
511                tkt.save_changes(self.author, body_text, when)
512                tkt['id'] = ticket_id
513
514                if self.VERSION  == 0.9:
515                        str = self.attachments(m, tkt, True)
516                else:
517                        str = self.attachments(m, tkt)
518
519                if self.notification:
520                        self.notify(tkt, False, when)
521
522                return True
523
524        def new_ticket(self, msg):
525                """
526                Create a new ticket
527                """
528                tkt = Ticket(self.env)
529                tkt['status'] = 'new'
530
531                # Some defaults
532                #
533                tkt['milestone'] = self.get_config('ticket', 'default_milestone')
534                tkt['priority'] = self.get_config('ticket', 'default_priority')
535                tkt['severity'] = self.get_config('ticket', 'default_severity')
536                tkt['version'] = self.get_config('ticket', 'default_version')
537
538                if not msg['Subject']:
539                        tkt['summary'] = u'(No subject)'
540                else:
541                        tkt['summary'] = self.email_to_unicode(msg['Subject'])
542
543
544                if settings.has_key('component'):
545                        tkt['component'] = settings['component']
546                else:
547                        tkt['component'] = self.spam(msg)
548
549                # Discard SPAM messages.
550                #
551                if self.DROP_SPAM and (tkt['component'] == 'Spam'):
552                        if self.DEBUG > 2 :
553                          print 'This message is a SPAM. Automatic ticket insertion refused (SPAM level > %d' % self.SPAM_LEVEL
554                        return False   
555
556                # Set default owner for component, HvB
557                # Is not necessary, because if component is set. The trac code
558                # will find the owner: self.set_owner(tkt)
559                #
560                self.set_reply_fields(tkt, msg)
561
562                # produce e-mail like header
563                #
564                head = ''
565                if self.EMAIL_HEADER > 0:
566                        head = self.email_header_txt(msg)
567                       
568                body_text = self.get_body_text(msg)
569
570                tkt['description'] = '\r\n%s\r\n%s' \
571                        %(head, body_text)
572
573                when = int(time.time())
574
575                ticket_id = tkt.insert()
576                tkt['id'] = ticket_id
577
578                changed = False
579                comment = ''
580
581                # Rewrite the description if we have mailto enabled
582                #
583                if self.MAILTO:
584                        changed = True
585                        comment = u'\nadded mailto line\n'
586                        mailto = self.html_mailto_link(tkt['summary'], ticket_id, body_text)
587                        tkt['description'] = u'\r\n%s\r\n%s%s\r\n' \
588                                %(head, mailto, body_text)
589
590                str =  self.attachments(msg, tkt)
591                if str:
592                        changed = True
593                        comment = '%s\n%s\n' %(comment, str)
594
595                if changed:
596                        tkt.save_changes(self.author, comment)
597                        #print tkt.get_changelog(self.db, when)
598
599                if self.notification:
600                        self.notify(tkt, True)
601                        #self.notify(tkt, False)
602
603        def parse(self, fp):
604                global m
605
606                m = email.message_from_file(fp)
607                if not m:
608                        return
609
610                if self.DEBUG > 1:        # save the entire e-mail message text
611                        self.save_email_for_debug(m)
612                        self.debug_attachments(m)
613
614                self.db = self.env.get_db_cnx()
615                self.get_author_emailaddrs(m)
616
617                if self.blacklisted_from():
618                        if self.DEBUG > 1 :
619                                print 'Message rejected : From: in blacklist'
620                        return False
621
622                if self.get_config('notification', 'smtp_enabled') in ['true']:
623                        self.notification = 1
624                else:
625                        self.notification = 0
626
627                # Must we update existing tickets
628                #
629                if self.TICKET_UPDATE > 0:
630                        if self.ticket_update(m):
631                                return True
632
633                self.new_ticket(m)
634
635        def strip_signature(self, text):
636                """
637                Strip signature from message, inspired by Mailman software
638                """
639                body = []
640                for line in text.splitlines():
641                        if line == '-- ':
642                                break
643                        body.append(line)
644
645                return ('\n'.join(body))
646
647
648        def wrap_text(self, text, replace_whitespace = False):
649                """
650                Will break a lines longer then given length into several small lines of size
651                given length
652                """
653                import textwrap
654
655                LINESEPARATOR = '\n'
656                reformat = ''
657
658                for s in text.split(LINESEPARATOR):
659                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
660                        if tmp:
661                                reformat = '%s\n%s' %(reformat,tmp)
662                        else:
663                                reformat = '%s\n' %reformat
664
665                return reformat
666
667                # Python2.4 and higher
668                #
669                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
670                #
671
672
673        def get_body_text(self, msg):
674                """
675                put the message text in the ticket description or in the changes field.
676                message text can be plain text or html or something else
677                """
678                has_description = 0
679                encoding = True
680                ubody_text = u'No plain text message'
681                for part in msg.walk():
682
683                        # 'multipart/*' is a container for multipart messages
684                        #
685                        if part.get_content_maintype() == 'multipart':
686                                continue
687
688                        if part.get_content_type() == 'text/plain':
689                                # Try to decode, if fails then do not decode
690                                #
691                                body_text = part.get_payload(decode=1)
692                                if not body_text:                       
693                                        body_text = part.get_payload(decode=0)
694       
695                                if self.STRIP_SIGNATURE:
696                                        body_text = self.strip_signature(body_text)
697
698                                if self.USE_TEXTWRAP:
699                                        body_text = self.wrap_text(body_text)
700
701                                # Get contents charset (iso-8859-15 if not defined in mail headers)
702                                #
703                                charset = part.get_content_charset()
704                                if not charset:
705                                        charset = 'iso-8859-15'
706
707                                try:
708                                        ubody_text = unicode(body_text, charset)
709
710                                except UnicodeError, detail:
711                                        ubody_text = unicode(body_text, 'iso-8859-15')
712
713                                except LookupError, detail:
714                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
715
716                        elif part.get_content_type() == 'text/html':
717                                ubody_text = '(see attachment for HTML mail message)'
718
719                        else:
720                                ubody_text = '(see attachment for message)'
721
722                        has_description = 1
723                        break           # we have the description, so break
724
725                if not has_description:
726                        ubody_text = '(see attachment for message)'
727
728                # A patch so that the web-interface will not update the description
729                # field of a ticket
730                #
731                ubody_text = ('\r\n'.join(ubody_text.splitlines()))
732
733                #  If we can unicode it try to encode it for trac
734                #  else we a lot of garbage
735                #
736                #if encoding:
737                #       ubody_text = ubody_text.encode('utf-8')
738
739                if self.VERBATIM_FORMAT:
740                        ubody_text = '{{{\r\n%s\r\n}}}' %ubody_text
741                else:
742                        ubody_text = '%s' %ubody_text
743
744                return ubody_text
745
746        def notify(self, tkt , new=True, modtime=0):
747                """
748                A wrapper for the TRAC notify function. So we can use templates
749                """
750                if tkt['component'] == 'Spam':
751                        return 
752
753                try:
754                        # create false {abs_}href properties, to trick Notify()
755                        #
756                        self.env.abs_href = Href(self.get_config('project', 'url'))
757                        self.env.href = Href(self.get_config('project', 'url'))
758
759                        tn = TicketNotifyEmail(self.env)
760                        if self.notify_template:
761                                tn.template_name = self.notify_template;
762
763                        tn.notify(tkt, new, modtime)
764
765                except Exception, e:
766                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(tkt['id'], e)
767
768        def mail_line(self, str):
769                return '%s %s' % (self.comment, str)
770
771
772        def html_mailto_link(self, subject, id, body):
773                if not self.author:
774                        author = self.email_addr
775                else:   
776                        author = self.author
777
778                # Must find a fix
779                #
780                #arr = string.split(body, '\n')
781                #arr = map(self.mail_line, arr)
782                #body = string.join(arr, '\n')
783                #body = '%s wrote:\n%s' %(author, body)
784
785                # Temporary fix
786                #
787                str = 'mailto:%s?Subject=%s&Cc=%s' %(
788                       urllib.quote(self.email_addr),
789                           urllib.quote('Re: #%s: %s' %(id, subject)),
790                           urllib.quote(self.MAILTO_CC)
791                           )
792
793                str = '\r\n{{{\r\n#!html\r\n<a href="%s">Reply to: %s</a>\r\n}}}\r\n' %(str, author)
794                return str
795
796        def attachments(self, message, ticket, update=False):
797                '''
798                save any attachments as files in the ticket's directory
799                '''
800                count = 0
801                first = 0
802                number = 0
803
804                # Get Maxium attachment size
805                #
806                max_size = int(self.get_config('attachment', 'max_size'))
807                status   = ''
808
809                for part in message.walk():
810                        if part.get_content_maintype() == 'multipart':          # multipart/* is just a container
811                                continue
812
813                        if not first:                                                                           # first content is the message
814                                first = 1
815                                if part.get_content_type() == 'text/plain':             # if first is text, is was already put in the description
816                                        continue
817
818                        filename = part.get_filename()
819                        if not filename:
820                                number = number + 1
821                                filename = 'part%04d' % number
822
823                                ext = mimetypes.guess_extension(part.get_content_type())
824                                if not ext:
825                                        ext = '.bin'
826
827                                filename = '%s%s' % (filename, ext)
828                        else:
829                                filename = self.email_to_unicode(filename)
830
831                        # From the trac code
832                        #
833                        filename = filename.replace('\\', '/').replace(':', '/')
834                        filename = os.path.basename(filename)
835
836                        # We try to normalize the filename to utf-8 NFC if we can.
837                        # Files uploaded from OS X might be in NFD.
838                        # Check python version and then try it
839                        #
840                        if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
841                                try:
842                                        filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
843                                except TypeError:
844                                        pass
845
846                        url_filename = urllib.quote(filename)
847                        #
848                        # Must be tuneables HvB
849                        #
850                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, url_filename))
851                        text = part.get_payload(decode=1)
852                        if not text:
853                                text = '(None)'
854                        fd.write(text)
855                        fd.close()
856
857                        # get the file_size
858                        #
859                        stats = os.lstat(path)
860                        file_size = stats[stat.ST_SIZE]
861
862                        # Check if the attachment size is allowed
863                        #
864                        if (max_size != -1) and (file_size > max_size):
865                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
866                                        %(status, filename, file_size, max_size)
867
868                                os.unlink(path)
869                                continue
870                        else:
871                                count = count + 1
872                                       
873                        # Insert the attachment
874                        #
875                        fd = open(path)
876                        att = attachment.Attachment(self.env, 'ticket', ticket['id'])
877
878                        # This will break the ticket_update system, the body_text is vaporized
879                        # ;-(
880                        #
881                        if not update:
882                                att.author = self.author
883                                att.description = self.email_to_unicode('Added by email2trac')
884
885                        att.insert(url_filename, fd, file_size)
886                        #except  util.TracError, detail:
887                        #       print detail
888
889                        # Remove the created temporary filename
890                        #
891                        fd.close()
892                        os.unlink(path)
893
894                # Return how many attachments
895                #
896                status = 'This message has %d attachment(s)\n%s' %(count, status)
897                return status
898
899
900def mkdir_p(dir, mode):
901        '''do a mkdir -p'''
902
903        arr = string.split(dir, '/')
904        path = ''
905        for part in arr:
906                path = '%s/%s' % (path, part)
907                try:
908                        stats = os.stat(path)
909                except OSError:
910                        os.mkdir(path, mode)
911
912
913def ReadConfig(file, name):
914        """
915        Parse the config file
916        """
917
918        if not os.path.isfile(file):
919                print 'File %s does not exist' %file
920                sys.exit(1)
921
922        config = ConfigParser.ConfigParser()
923        try:
924                config.read(file)
925        except ConfigParser.MissingSectionHeaderError,detail:
926                print detail
927                sys.exit(1)
928
929
930        # Use given project name else use defaults
931        #
932        if name:
933                if not config.has_section(name):
934                        print "Not a valid project name: %s" %name
935                        print "Valid names: %s" %config.sections()
936                        sys.exit(1)
937
938                project =  dict()
939                for option in  config.options(name):
940                        project[option] = config.get(name, option)
941
942        else:
943                project = config.defaults()
944
945        return project
946
947
948if __name__ == '__main__':
949        # Default config file
950        #
951        configfile = '@email2trac_conf@'
952        project = ''
953        component = ''
954        ENABLE_SYSLOG = 0
955               
956        try:
957                opts, args = getopt.getopt(sys.argv[1:], 'chf:p:', ['component=','help', 'file=', 'project='])
958        except getopt.error,detail:
959                print __doc__
960                print detail
961                sys.exit(1)
962       
963        project_name = None
964        for opt,value in opts:
965                if opt in [ '-h', '--help']:
966                        print __doc__
967                        sys.exit(0)
968                elif opt in ['-c', '--component']:
969                        component = value
970                elif opt in ['-f', '--file']:
971                        configfile = value
972                elif opt in ['-p', '--project']:
973                        project_name = value
974       
975        settings = ReadConfig(configfile, project_name)
976        if not settings.has_key('project'):
977                print __doc__
978                print 'No Trac project is defined in the email2trac config file.'
979                sys.exit(1)
980       
981        if component:
982                settings['component'] = component
983       
984        if settings.has_key('trac_version'):
985                version = float(settings['trac_version'])
986        else:
987                version = trac_default_version
988
989        if settings.has_key('enable_syslog'):
990                ENABLE_SYSLOG =  float(settings['enable_syslog'])
991                       
992        #debug HvB
993        #print settings
994       
995        try:
996                if version == 0.9:
997                        from trac import attachment
998                        from trac.env import Environment
999                        from trac.ticket import Ticket
1000                        from trac.web.href import Href
1001                        from trac import util
1002                        from trac.Notify import TicketNotifyEmail
1003                elif version == 0.10:
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                        #
1010                        # return  util.text.to_unicode(str)
1011                        #
1012                        # see http://projects.edgewall.com/trac/changeset/2799
1013                        from trac.ticket.notification import TicketNotifyEmail
1014       
1015                env = Environment(settings['project'], create=0)
1016                tktparser = TicketEmailParser(env, settings, version)
1017                tktparser.parse(sys.stdin)
1018
1019        # Catch all errors ans log to SYSLOG if we have enabled this
1020        # else stdout
1021        #
1022        except Exception, error:
1023                if ENABLE_SYSLOG:
1024                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
1025                        etype, evalue, etb = sys.exc_info()
1026                        for e in traceback.format_exception(etype, evalue, etb):
1027                                syslog.syslog(e)
1028                        syslog.closelog()
1029                else:
1030                        traceback.print_exc()
1031
1032                if m:
1033                        tktparser.save_email_for_debug(m, True)
1034
1035# EOB
Note: See TracBrowser for help on using the repository browser.