source: trunk/email2trac.py.in @ 190

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

email2trac.py.in:

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