source: trunk/email2trac.py.in @ 191

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

email2trac.py.in, ChangeLog?, email2trac.conf:

  • Added strip_quotes function. Author: nicolasm at opera dot com
  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 27.5 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 191 2007-10-11 10:01:11Z bas $
75"""
76import os
77import sys
78import string
79import getopt
80import stat
81import time
82import email
83import email.Iterators
84import email.Header
85import re
86import urllib
87import unicodedata
88import ConfigParser
89from stat import *
90import mimetypes
91import 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                #
542                when = int(time.time())
543
544                try:
545                        tkt = Ticket(self.env, ticket_id, self.db)
546                except util.TracError, detail:
547                        return False
548
549                # Must we update some ticket fields properties
550                #
551                if update_tkt_fields:
552                        self.update_ticket_fields(tkt, update_tkt_fields)
553
554                body_text = self.get_body_text(m)
555                if self.EMAIL_HEADER:
556                        head = self.email_header_txt(m)
557                        body_text = u"\r\n%s \r\n%s" %(head, body_text)
558
559                #if self.MAILTO:
560                #       mailto = self.html_mailto_link(tkt['summary'], ticket_id, body_text)
561                #       body_text = u"\r\n%s \r\n%s" %(mailto, body_text)
562
563                tkt.save_changes(self.author, body_text, when)
564                tkt['id'] = ticket_id
565
566                if self.VERSION  == 0.9:
567                        str = self.attachments(m, tkt, True)
568                else:
569                        str = self.attachments(m, tkt)
570
571                if self.notification:
572                        self.notify(tkt, False, when)
573
574                return True
575
576        def new_ticket(self, msg):
577                """
578                Create a new ticket
579                """
580                tkt = Ticket(self.env)
581                tkt['status'] = 'new'
582
583                # Some defaults
584                #
585                tkt['milestone'] = self.get_config('ticket', 'default_milestone')
586                tkt['priority'] = self.get_config('ticket', 'default_priority')
587                tkt['severity'] = self.get_config('ticket', 'default_severity')
588                tkt['version'] = self.get_config('ticket', 'default_version')
589
590                if not msg['Subject']:
591                        tkt['summary'] = u'(No subject)'
592                else:
593                        tkt['summary'] = self.email_to_unicode(msg['Subject'])
594
595
596                if settings.has_key('component'):
597                        tkt['component'] = settings['component']
598                else:
599                        tkt['component'] = self.spam(msg)
600
601                # Discard SPAM messages.
602                #
603                if self.DROP_SPAM and (tkt['component'] == 'Spam'):
604                        if self.DEBUG > 2 :
605                          print 'This message is a SPAM. Automatic ticket insertion refused (SPAM level > %d' % self.SPAM_LEVEL
606                        return False   
607
608                # Set default owner for component, HvB
609                # Is not necessary, because if component is set. The trac code
610                # will find the owner: self.set_owner(tkt)
611                #
612                self.set_reply_fields(tkt, msg)
613
614                # produce e-mail like header
615                #
616                head = ''
617                if self.EMAIL_HEADER > 0:
618                        head = self.email_header_txt(msg)
619                       
620                body_text = self.get_body_text(msg)
621
622                tkt['description'] = '\r\n%s\r\n%s' \
623                        %(head, body_text)
624
625                #when = int(time.time())
626                utc = UTC()
627                when = datetime.now(utc)
628
629                ticket_id = tkt.insert()
630                #try:
631                #       ticket_id = tkt.insert()
632                #except OperationalError, detail:
633                #       syslog.openlog('email2trac', syslog.LOG_NOWAIT)
634                #       syslog.syslog('catch tkt insert problem %s' %detail)
635                #       syslog.closelog()
636                #
637                #       ticket_id = tkt.insert()
638                       
639                tkt['id'] = ticket_id
640
641                changed = False
642                comment = ''
643
644                # Rewrite the description if we have mailto enabled
645                #
646                if self.MAILTO:
647                        changed = True
648                        comment = u'\nadded mailto line\n'
649                        mailto = self.html_mailto_link(tkt['summary'], ticket_id, body_text)
650                        tkt['description'] = u'\r\n%s\r\n%s%s\r\n' \
651                                %(head, mailto, body_text)
652
653                str =  self.attachments(msg, tkt)
654                if str:
655                        changed = True
656                        comment = '%s\n%s\n' %(comment, str)
657
658                if changed:
659                        tkt.save_changes(self.author, comment)
660                        #print tkt.get_changelog(self.db, when)
661
662                if self.notification:
663                        self.notify(tkt, True)
664                        #self.notify(tkt, False)
665
666        def parse(self, fp):
667                global m
668
669                m = email.message_from_file(fp)
670                if not m:
671                        return
672
673                if self.DEBUG > 1:        # save the entire e-mail message text
674                        self.save_email_for_debug(m)
675                        self.debug_attachments(m)
676
677                self.db = self.env.get_db_cnx()
678                self.get_author_emailaddrs(m)
679
680                if self.blacklisted_from():
681                        if self.DEBUG > 1 :
682                                print 'Message rejected : From: in blacklist'
683                        return False
684
685                if self.get_config('notification', 'smtp_enabled') in ['true']:
686                        self.notification = 1
687                else:
688                        self.notification = 0
689
690                # Must we update existing tickets
691                #
692                if self.TICKET_UPDATE > 0:
693                        if self.ticket_update(m):
694                                return True
695
696                self.new_ticket(m)
697
698        def strip_signature(self, text):
699                """
700                Strip signature from message, inspired by Mailman software
701                """
702                body = []
703                for line in text.splitlines():
704                        if line == '-- ':
705                                break
706                        body.append(line)
707
708                return ('\n'.join(body))
709
710        def strip_quotes(self, text):
711        """
712        Strip quotes from message by Nicolas Mendoza
713        """
714        body = []
715        for line in text.splitlines():
716                if line.startswith(self.EMAIL_QUOTE):
717                        continue
718                body.append(line)
719
720        return ('\n'.join(body))
721
722
723        def wrap_text(self, text, replace_whitespace = False):
724                """
725                Will break a lines longer then given length into several small
726                lines of size given length
727                """
728                import textwrap
729
730                LINESEPARATOR = '\n'
731                reformat = ''
732
733                for s in text.split(LINESEPARATOR):
734                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
735                        if tmp:
736                                reformat = '%s\n%s' %(reformat,tmp)
737                        else:
738                                reformat = '%s\n' %reformat
739
740                return reformat
741
742                # Python2.4 and higher
743                #
744                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
745                #
746
747
748        def get_body_text(self, msg):
749                """
750                put the message text in the ticket description or in the changes field.
751                message text can be plain text or html or something else
752                """
753                has_description = 0
754                encoding = True
755                ubody_text = u'No plain text message'
756                for part in msg.walk():
757
758                        # 'multipart/*' is a container for multipart messages
759                        #
760                        if part.get_content_maintype() == 'multipart':
761                                continue
762
763                        if part.get_content_type() == 'text/plain':
764                                # Try to decode, if fails then do not decode
765                                #
766                                body_text = part.get_payload(decode=1)
767                                if not body_text:                       
768                                        body_text = part.get_payload(decode=0)
769       
770                                if self.STRIP_SIGNATURE:
771                                        body_text = self.strip_signature(body_text)
772
773                                if self.STRIP_QUOTES:
774                                        body_text = self.strip_quotes(body_text)
775
776                                if self.USE_TEXTWRAP:
777                                        body_text = self.wrap_text(body_text)
778
779                                # Get contents charset (iso-8859-15 if not defined in mail headers)
780                                #
781                                charset = part.get_content_charset()
782                                if not charset:
783                                        charset = 'iso-8859-15'
784
785                                try:
786                                        ubody_text = unicode(body_text, charset)
787
788                                except UnicodeError, detail:
789                                        ubody_text = unicode(body_text, 'iso-8859-15')
790
791                                except LookupError, detail:
792                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
793
794                        elif part.get_content_type() == 'text/html':
795                                ubody_text = '(see attachment for HTML mail message)'
796
797                        else:
798                                ubody_text = '(see attachment for message)'
799
800                        has_description = 1
801                        break           # we have the description, so break
802
803                if not has_description:
804                        ubody_text = '(see attachment for message)'
805
806                # A patch so that the web-interface will not update the description
807                # field of a ticket
808                #
809                ubody_text = ('\r\n'.join(ubody_text.splitlines()))
810
811                #  If we can unicode it try to encode it for trac
812                #  else we a lot of garbage
813                #
814                #if encoding:
815                #       ubody_text = ubody_text.encode('utf-8')
816
817                if self.VERBATIM_FORMAT:
818                        ubody_text = '{{{\r\n%s\r\n}}}' %ubody_text
819                else:
820                        ubody_text = '%s' %ubody_text
821
822                return ubody_text
823
824        def notify(self, tkt , new=True, modtime=0):
825                """
826                A wrapper for the TRAC notify function. So we can use templates
827                """
828                if tkt['component'] == 'Spam':
829                        return 
830
831                try:
832                        # create false {abs_}href properties, to trick Notify()
833                        #
834                        self.env.abs_href = Href(self.get_config('project', 'url'))
835                        self.env.href = Href(self.get_config('project', 'url'))
836
837                        tn = TicketNotifyEmail(self.env)
838                        if self.notify_template:
839                                tn.template_name = self.notify_template;
840
841                        tn.notify(tkt, new, modtime)
842
843                except Exception, e:
844                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(tkt['id'], e)
845
846        def html_mailto_link(self, subject, id, body):
847                if not self.author:
848                        author = self.email_addr
849                else:   
850                        author = self.author
851
852                # Must find a fix
853                #
854                #arr = string.split(body, '\n')
855                #arr = map(self.mail_line, arr)
856                #body = string.join(arr, '\n')
857                #body = '%s wrote:\n%s' %(author, body)
858
859                # Temporary fix
860                #
861                str = 'mailto:%s?Subject=%s&Cc=%s' %(
862                       urllib.quote(self.email_addr),
863                           urllib.quote('Re: #%s: %s' %(id, subject)),
864                           urllib.quote(self.MAILTO_CC)
865                           )
866
867                str = '\r\n{{{\r\n#!html\r\n<a href="%s">Reply to: %s</a>\r\n}}}\r\n' %(str, author)
868                return str
869
870        def attachments(self, message, ticket, update=False):
871                '''
872                save any attachments as files in the ticket's directory
873                '''
874                count = 0
875                first = 0
876                number = 0
877
878                # Get Maxium attachment size
879                #
880                max_size = int(self.get_config('attachment', 'max_size'))
881                status   = ''
882
883                for part in message.walk():
884                        if part.get_content_maintype() == 'multipart':          # multipart/* is just a container
885                                continue
886
887                        if not first:                                                                           # first content is the message
888                                first = 1
889                                if part.get_content_type() == 'text/plain':             # if first is text, is was already put in the description
890                                        continue
891
892                        filename = part.get_filename()
893                        if not filename:
894                                number = number + 1
895                                filename = 'part%04d' % number
896
897                                ext = mimetypes.guess_extension(part.get_content_type())
898                                if not ext:
899                                        ext = '.bin'
900
901                                filename = '%s%s' % (filename, ext)
902                        else:
903                                filename = self.email_to_unicode(filename)
904
905                        # From the trac code
906                        #
907                        filename = filename.replace('\\', '/').replace(':', '/')
908                        filename = os.path.basename(filename)
909
910                        # We try to normalize the filename to utf-8 NFC if we can.
911                        # Files uploaded from OS X might be in NFD.
912                        # Check python version and then try it
913                        #
914                        if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
915                                try:
916                                        filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
917                                except TypeError:
918                                        pass
919
920                        url_filename = urllib.quote(filename)
921                        #
922                        # Must be tuneables HvB
923                        #
924                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, url_filename))
925                        text = part.get_payload(decode=1)
926                        if not text:
927                                text = '(None)'
928                        fd.write(text)
929                        fd.close()
930
931                        # get the file_size
932                        #
933                        stats = os.lstat(path)
934                        file_size = stats[stat.ST_SIZE]
935
936                        # Check if the attachment size is allowed
937                        #
938                        if (max_size != -1) and (file_size > max_size):
939                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
940                                        %(status, filename, file_size, max_size)
941
942                                os.unlink(path)
943                                continue
944                        else:
945                                count = count + 1
946                                       
947                        # Insert the attachment
948                        #
949                        fd = open(path)
950                        att = attachment.Attachment(self.env, 'ticket', ticket['id'])
951
952                        # This will break the ticket_update system, the body_text is vaporized
953                        # ;-(
954                        #
955                        if not update:
956                                att.author = self.author
957                                att.description = self.email_to_unicode('Added by email2trac')
958
959                        att.insert(url_filename, fd, file_size)
960                        #except  util.TracError, detail:
961                        #       print detail
962
963                        # Remove the created temporary filename
964                        #
965                        fd.close()
966                        os.unlink(path)
967
968                # Return how many attachments
969                #
970                status = 'This message has %d attachment(s)\n%s' %(count, status)
971                return status
972
973
974def mkdir_p(dir, mode):
975        '''do a mkdir -p'''
976
977        arr = string.split(dir, '/')
978        path = ''
979        for part in arr:
980                path = '%s/%s' % (path, part)
981                try:
982                        stats = os.stat(path)
983                except OSError:
984                        os.mkdir(path, mode)
985
986
987def ReadConfig(file, name):
988        """
989        Parse the config file
990        """
991
992        if not os.path.isfile(file):
993                print 'File %s does not exist' %file
994                sys.exit(1)
995
996        config = ConfigParser.ConfigParser()
997        try:
998                config.read(file)
999        except ConfigParser.MissingSectionHeaderError,detail:
1000                print detail
1001                sys.exit(1)
1002
1003
1004        # Use given project name else use defaults
1005        #
1006        if name:
1007                if not config.has_section(name):
1008                        print "Not a valid project name: %s" %name
1009                        print "Valid names: %s" %config.sections()
1010                        sys.exit(1)
1011
1012                project =  dict()
1013                for option in  config.options(name):
1014                        project[option] = config.get(name, option)
1015
1016        else:
1017                project = config.defaults()
1018
1019        return project
1020
1021
1022if __name__ == '__main__':
1023        # Default config file
1024        #
1025        configfile = '@email2trac_conf@'
1026        project = ''
1027        component = ''
1028        ENABLE_SYSLOG = 0
1029               
1030        try:
1031                opts, args = getopt.getopt(sys.argv[1:], 'chf:p:', ['component=','help', 'file=', 'project='])
1032        except getopt.error,detail:
1033                print __doc__
1034                print detail
1035                sys.exit(1)
1036       
1037        project_name = None
1038        for opt,value in opts:
1039                if opt in [ '-h', '--help']:
1040                        print __doc__
1041                        sys.exit(0)
1042                elif opt in ['-c', '--component']:
1043                        component = value
1044                elif opt in ['-f', '--file']:
1045                        configfile = value
1046                elif opt in ['-p', '--project']:
1047                        project_name = value
1048       
1049        settings = ReadConfig(configfile, project_name)
1050        if not settings.has_key('project'):
1051                print __doc__
1052                print 'No Trac project is defined in the email2trac config file.'
1053                sys.exit(1)
1054       
1055        if component:
1056                settings['component'] = component
1057       
1058        if settings.has_key('trac_version'):
1059                version = settings['trac_version']
1060        else:
1061                version = trac_default_version
1062
1063
1064        #debug HvB
1065        #print settings
1066
1067        try:
1068                if version == '0.9':
1069                        from trac import attachment
1070                        from trac.env import Environment
1071                        from trac.ticket import Ticket
1072                        from trac.web.href import Href
1073                        from trac import util
1074                        from trac.Notify import TicketNotifyEmail
1075                elif version == '0.10':
1076                        from trac import attachment
1077                        from trac.env import Environment
1078                        from trac.ticket import Ticket
1079                        from trac.web.href import Href
1080                        from trac import util
1081                        #
1082                        # return  util.text.to_unicode(str)
1083                        #
1084                        # see http://projects.edgewall.com/trac/changeset/2799
1085                        from trac.ticket.notification import TicketNotifyEmail
1086                elif version == '0.11':
1087                        from trac import attachment
1088                        from trac.env import Environment
1089                        from trac.ticket import Ticket
1090                        from trac.web.href import Href
1091                        from trac import util
1092                        #
1093                        # return  util.text.to_unicode(str)
1094                        #
1095                        # see http://projects.edgewall.com/trac/changeset/2799
1096                        from trac.ticket.notification import TicketNotifyEmail
1097                else:
1098                        print 'TRAC version %s is not supported' %version
1099                        sys.exit(1)
1100                       
1101                if settings.has_key('enable_syslog'):
1102                        if SYSLOG_AVAILABLE:
1103                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
1104
1105                env = Environment(settings['project'], create=0)
1106                tktparser = TicketEmailParser(env, settings, float(version))
1107                tktparser.parse(sys.stdin)
1108
1109        # Catch all errors ans log to SYSLOG if we have enabled this
1110        # else stdout
1111        #
1112        except Exception, error:
1113                if ENABLE_SYSLOG:
1114                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
1115
1116                        etype, evalue, etb = sys.exc_info()
1117                        for e in traceback.format_exception(etype, evalue, etb):
1118                                syslog.syslog(e)
1119
1120                        syslog.closelog()
1121                else:
1122                        traceback.print_exc()
1123
1124                if m:
1125                        tktparser.save_email_for_debug(m, True)
1126
1127# EOB
Note: See TracBrowser for help on using the repository browser.