source: trunk/email2trac.py.in @ 193

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

email2trac.py.in:

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