source: trunk/email2trac.py.in @ 184

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

email2trac.py.in:

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