source: trunk/email2trac.py.in @ 201

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

email2trac.py.in:

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