source: trunk/email2trac.py.in @ 172

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

email2trac.py.in:

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