source: trunk/email2trac.py.in @ 206

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

email2trac.py.in:

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