source: trunk/email2trac.py.in @ 192

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

email2trac.py.in,

  • Added two patches that must fix the notify function for trac version 0.11

ChangeLog?:

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