source: trunk/email2trac.py.in @ 189

Last change on this file since 189 was 189, checked in by bas, 13 years ago

email2trac.py.in:

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