source: trunk/email2trac.py.in @ 339

Last change on this file since 339 was 339, checked in by bas, 14 years ago

email2trac.py.in:

  • removed obsolete function mkdir_p
    • re-arranged code is sections
  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 43.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
30Authors:
31  Bas van der Vlies <basv@sara.nl>
32  Walter de Jong <walter@sara.nl>
33
34The scripts reads emails from stdin and inserts directly into a Trac database.
35
36How to use
37----------
38 * See https://subtrac.sara.nl/oss/email2trac/
39
40 * Create an config file:
41    [DEFAULT]                        # REQUIRED
42    project      : /data/trac/test   # REQUIRED
43    debug        : 1                 # OPTIONAL, if set print some DEBUG info
44    trac_version : 0.10              # OPTIONAL, default is 0.11
45
46    [jouvin]                         # OPTIONAL project declaration, if set both fields necessary
47    project      : /data/trac/jouvin # use -p|--project jouvin. 
48       
49 * default config file is : /etc/email2trac.conf
50
51 * Commandline opions:
52                -h,--help
53                -f,--file  <configuration file>
54                -n,--dry-run
55                -p, --project <project name>
56                -t, --ticket_prefix <name>
57
58SVN Info:
59        $Id: email2trac.py.in 339 2010-03-24 14:06:51Z bas $
60"""
61import os
62import sys
63import string
64import getopt
65import stat
66import time
67import email
68import email.Iterators
69import email.Header
70import re
71import urllib
72import unicodedata
73from stat import *
74import mimetypes
75import traceback
76
77
78# Will fail where unavailable, e.g. Windows
79#
80try:
81    import syslog
82    SYSLOG_AVAILABLE = True
83except ImportError:
84    SYSLOG_AVAILABLE = False
85
86from datetime import tzinfo, timedelta, datetime
87from trac import config as trac_config
88
89# Some global variables
90#
91trac_default_version = '0.11'
92m = None
93
94# A UTC class needed for trac version 0.11, added by
95# tbaschak at ktc dot mb dot ca
96#
97class UTC(tzinfo):
98        """UTC"""
99        ZERO = timedelta(0)
100        HOUR = timedelta(hours=1)
101       
102        def utcoffset(self, dt):
103                return self.ZERO
104               
105        def tzname(self, dt):
106                return "UTC"
107               
108        def dst(self, dt):
109                return self.ZERO
110
111
112class TicketEmailParser(object):
113        env = None
114        comment = '> '
115   
116        def __init__(self, env, parameters, version):
117                self.env = env
118
119                # Database connection
120                #
121                self.db = None
122
123                # Save parameters
124                #
125                self.parameters = parameters
126
127                # Some useful mail constants
128                #
129                self.email_name = None
130                self.email_addr = None
131                self.email_from = None
132                self.author     = None
133                self.id         = None
134               
135                self.STRIP_CONTENT_TYPES = list()
136
137                self.VERSION = version
138                self.DRY_RUN = parameters['dry_run']
139                self.VERBOSE = parameters['verbose']
140
141                self.get_config = self.env.config.get
142
143                if parameters.has_key('umask'):
144                        os.umask(int(parameters['umask'], 8))
145
146                if parameters.has_key('debug'):
147                        self.DEBUG = int(parameters['debug'])
148                else:
149                        self.DEBUG = 0
150
151                if parameters.has_key('mailto_link'):
152                        self.MAILTO = int(parameters['mailto_link'])
153                        if parameters.has_key('mailto_cc'):
154                                self.MAILTO_CC = parameters['mailto_cc']
155                        else:
156                                self.MAILTO_CC = ''
157                else:
158                        self.MAILTO = 0
159
160                if parameters.has_key('spam_level'):
161                        self.SPAM_LEVEL = int(parameters['spam_level'])
162                else:
163                        self.SPAM_LEVEL = 0
164
165                if parameters.has_key('spam_header'):
166                        self.SPAM_HEADER = parameters['spam_header']
167                else:
168                        self.SPAM_HEADER = 'X-Spam-Score'
169
170                if parameters.has_key('email_quote'):
171                        self.EMAIL_QUOTE = str(parameters['email_quote'])
172                else:   
173                        self.EMAIL_QUOTE = '> '
174
175                if parameters.has_key('email_header'):
176                        self.EMAIL_HEADER = int(parameters['email_header'])
177                else:
178                        self.EMAIL_HEADER = 0
179
180                if parameters.has_key('alternate_notify_template'):
181                        self.notify_template = str(parameters['alternate_notify_template'])
182                else:
183                        self.notify_template = None
184
185                if parameters.has_key('alternate_notify_template_update'):
186                        self.notify_template_update = str(parameters['alternate_notify_template_update'])
187                else:
188                        self.notify_template_update = None
189
190                if parameters.has_key('reply_all'):
191                        self.REPLY_ALL = int(parameters['reply_all'])
192                else:
193                        self.REPLY_ALL = 0
194
195                if parameters.has_key('ticket_update'):
196                        self.TICKET_UPDATE = int(parameters['ticket_update'])
197                else:
198                        self.TICKET_UPDATE = 0
199
200                if parameters.has_key('drop_spam'):
201                        self.DROP_SPAM = int(parameters['drop_spam'])
202                else:
203                        self.DROP_SPAM = 0
204
205                if parameters.has_key('verbatim_format'):
206                        self.VERBATIM_FORMAT = int(parameters['verbatim_format'])
207                else:
208                        self.VERBATIM_FORMAT = 1
209
210                if parameters.has_key('reflow'):
211                        self.REFLOW = int(parameters['reflow'])
212                else:
213                        self.REFLOW = 1
214
215                if parameters.has_key('drop_alternative_html_version'):
216                        self.DROP_ALTERNATIVE_HTML_VERSION = int(parameters['drop_alternative_html_version'])
217                else:
218                        self.DROP_ALTERNATIVE_HTML_VERSION = 0
219
220                if parameters.has_key('strip_signature'):
221                        self.STRIP_SIGNATURE = int(parameters['strip_signature'])
222                else:
223                        self.STRIP_SIGNATURE = 0
224
225                if parameters.has_key('strip_quotes'):
226                        self.STRIP_QUOTES = int(parameters['strip_quotes'])
227                else:
228                        self.STRIP_QUOTES = 0
229
230                self.properties = dict()
231                if parameters.has_key('inline_properties'):
232                        self.INLINE_PROPERTIES = int(parameters['inline_properties'])
233                else:
234                        self.INLINE_PROPERTIES = 0
235
236                if parameters.has_key('use_textwrap'):
237                        self.USE_TEXTWRAP = int(parameters['use_textwrap'])
238                else:
239                        self.USE_TEXTWRAP = 0
240
241                if parameters.has_key('binhex'):
242                        self.STRIP_CONTENT_TYPES.append('application/mac-binhex40')
243
244                if parameters.has_key('applesingle'):
245                        self.STRIP_CONTENT_TYPES.append('application/applefile')
246
247                if parameters.has_key('appledouble'):
248                        self.STRIP_CONTENT_TYPES.append('application/applefile')
249
250                if parameters.has_key('strip_content_types'):
251                        items = parameters['strip_content_types'].split(',')
252                        for item in items:
253                                self.STRIP_CONTENT_TYPES.append(item.strip())
254
255                self.WORKFLOW = None
256                if parameters.has_key('workflow'):
257                        self.WORKFLOW = parameters['workflow']
258
259                # Use OS independend functions
260                #
261                self.TMPDIR = os.path.normcase('/tmp')
262                if parameters.has_key('tmpdir'):
263                        self.TMPDIR = os.path.normcase(str(parameters['tmpdir']))
264
265                if parameters.has_key('ignore_trac_user_settings'):
266                        self.IGNORE_TRAC_USER_SETTINGS = int(parameters['ignore_trac_user_settings'])
267                else:
268                        self.IGNORE_TRAC_USER_SETTINGS = 0
269
270                if parameters.has_key('email_triggers_workflow'):
271                        self.EMAIL_TRIGGERS_WORKFLOW = int(parameters['email_triggers_workflow'])
272                else:
273                        self.EMAIL_TRIGGERS_WORKFLOW = 1
274
275                if parameters.has_key('subject_field_separator'):
276                        self.SUBJECT_FIELD_SEPARATOR = parameters['subject_field_separator'].strip()
277                else:
278                        self.SUBJECT_FIELD_SEPARATOR = '&'
279
280                self.trac_smtp_from = self.get_config('notification', 'smtp_from')
281
282########## Email Header Field Functions ###########################################################
283
284        def spam(self, message):
285                """
286                # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
287                # Note if Spam_level then '*' are included
288                """
289                spam = False
290                if message.has_key(self.SPAM_HEADER):
291                        spam_l = string.split(message[self.SPAM_HEADER])
292
293                        try:
294                                number = spam_l[0].count('*')
295                        except IndexError, detail:
296                                number = 0
297                               
298                        if number >= self.SPAM_LEVEL:
299                                spam = True
300                               
301                # treat virus mails as spam
302                #
303                elif message.has_key('X-Virus-found'):                 
304                        spam = True
305
306                # How to handle SPAM messages
307                #
308                if self.DROP_SPAM and spam:
309                        if self.DEBUG > 2 :
310                                print 'This message is a SPAM. Automatic ticket insertion refused (SPAM level > %d' % self.SPAM_LEVEL
311
312                        return 'drop'   
313
314                elif spam:
315
316                        return 'Spam'   
317
318                else:
319
320                        return False
321
322        def email_header_acl(self, keyword, header_field, default):
323                """
324                This function wil check if the email address is allowed or denied
325                to send mail to the ticket list
326            """
327                try:
328                        mail_addresses = self.parameters[keyword]
329
330                        # Check if we have an empty string
331                        #
332                        if not mail_addresses:
333                                return default
334
335                except KeyError, detail:
336                        if self.DEBUG > 2 :
337                                print 'TD: %s not defined, all messages are allowed.' %(keyword)
338
339                        return default
340
341                mail_addresses = string.split(mail_addresses, ',')
342
343                for entry in mail_addresses:
344                        entry = entry.strip()
345                        TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
346                        result =  TO_RE.search(header_field)
347                        if result:
348                                return True
349
350                return False
351
352        def email_to_unicode(self, message_str):
353                """
354                Email has 7 bit ASCII code, convert it to unicode with the charset
355        that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
356                understands it.
357                """
358                if self.VERBOSE:
359                        print "VB: email_to_unicode"
360
361                results =  email.Header.decode_header(message_str)
362
363                s = None
364                for text,format in results:
365                        if format:
366                                try:
367                                        temp = unicode(text, format)
368                                except UnicodeError, detail:
369                                        # This always works
370                                        #
371                                        temp = unicode(text, 'iso-8859-15')
372                                except LookupError, detail:
373                                        #text = 'ERROR: Could not find charset: %s, please install' %format
374                                        #temp = unicode(text, 'iso-8859-15')
375                                        temp = message_str
376                                       
377                        else:
378                                temp = string.strip(text)
379                                temp = unicode(text, 'iso-8859-15')
380
381                        if s:
382                                s = '%s %s' %(s, temp)
383                        else:
384                                s = '%s' %temp
385
386                #s = s.encode('utf-8')
387                return s
388
389
390        def email_header_txt(self, m):
391                """
392                Display To and CC addresses in description field
393                """
394                s = ''
395
396                if m['To'] and len(m['To']) > 0:
397                        s = "'''To:''' %s\r\n" %(m['To'])
398                if m['Cc'] and len(m['Cc']) > 0:
399                        s = "%s'''Cc:''' %s\r\n" % (s, m['Cc'])
400
401                return  self.email_to_unicode(s)
402
403
404        def get_sender_info(self, message):
405                """
406                Get the default author name and email address from the message
407                """
408
409                self.email_to = self.email_to_unicode(message['to'])
410                self.to_name, self.to_email_addr = email.Utils.parseaddr (self.email_to)
411
412                self.email_from = self.email_to_unicode(message['from'])
413                self.email_name, self.email_addr  = email.Utils.parseaddr(self.email_from)
414
415                ## Trac can not handle author's name that contains spaces
416                #  and forbid the ticket email address as author field
417
418                if self.email_addr == self.trac_smtp_from:
419                        self.author = "email2trac"
420                else:
421                        self.author = self.email_addr
422
423                if self.IGNORE_TRAC_USER_SETTINGS:
424                        return
425
426                # Is this a registered user, use email address as search key:
427                # result:
428                #   u : login name
429                #   n : Name that the user has set in the settings tab
430                #   e : email address that the user has set in the settings tab
431                #
432                users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
433                        if e and (e.lower() == self.email_addr.lower()) ]
434
435                if len(users) == 1:
436                        self.email_from = users[0][0]
437                        self.author = users[0][0]
438
439        def set_reply_fields(self, ticket, message):
440                """
441                Set all the right fields for a new ticket
442                """
443                if self.DEBUG:
444                        print 'TD: set_reply_fields'
445
446                ## Only use name or email adress
447                #ticket['reporter'] = self.email_from
448                ticket['reporter'] = self.author
449
450
451                # Put all CC-addresses in ticket CC field
452                #
453                if self.REPLY_ALL:
454
455                        email_cc = ''
456
457                        cc_addrs = email.Utils.getaddresses( message.get_all('cc', []) )
458
459                        if not cc_addrs:
460                                return
461
462                        ## Build a list of forbidden CC addresses
463                        #
464                        #to_addrs = email.Utils.getaddresses( message.get_all('to', []) )
465                        #to_list = list()
466                        #for n,e in to_addrs:
467                        #       to_list.append(e)
468                               
469                        # Remove reporter email address if notification is
470                        # on
471                        #
472                        if self.notification:
473                                try:
474                                        cc_addrs.remove((self.author, self.email_addr))
475                                except ValueError, detail:
476                                        pass
477
478                        for name,addr in cc_addrs:
479               
480                                ## Prevent mail loop
481                                #
482                                #if addr in to_list:
483
484                                if addr == self.trac_smtp_from:
485                                        if self.DEBUG:
486                                                print "Skipping %s mail address for CC-field" %(addr)
487                                        continue
488
489                                if email_cc:
490                                        email_cc = '%s, %s' %(email_cc, addr)
491                                else:
492                                        email_cc = addr
493
494                        if email_cc:
495                                if self.DEBUG:
496                                        print 'TD: set_reply_fields: %s' %email_cc
497
498                                ticket['cc'] = self.email_to_unicode(email_cc)
499
500
501########## DEBUG functions  ###########################################################
502
503        def debug_body(self, message_body, tempfile=False):
504                if tempfile:
505                        import tempfile
506                        body_file = tempfile.mktemp('.email2trac')
507                else:
508                        body_file = os.path.join(self.TMPDIR, 'body.txt')
509
510                if self.DRY_RUN:
511                        print 'DRY-RUN: not saving body to %s' %(body_file)
512                        return
513
514                print 'TD: writing body to %s' %(body_file)
515                fx = open(body_file, 'wb')
516                if not message_body:
517                                message_body = '(None)'
518
519                message_body = message_body.encode('utf-8')
520                #message_body = unicode(message_body, 'iso-8859-15')
521
522                fx.write(message_body)
523                fx.close()
524                try:
525                        os.chmod(body_file,S_IRWXU|S_IRWXG|S_IRWXO)
526                except OSError:
527                        pass
528
529        def debug_attachments(self, message_parts):
530                """
531                """
532                if self.VERBOSE:
533                        print "VB: debug_attachments"
534               
535                n = 0
536                for item in message_parts:
537                        # Skip inline text parts
538                        if not isinstance(item, tuple):
539                                continue
540                               
541                        (original, filename, part) = item
542
543                        n = n + 1
544                        print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type())
545                        print 'TD: part%d: filename: %s' % (n, part.get_filename())
546
547                        part_file = os.path.join(self.TMPDIR, part.get_filename())
548                        print 'TD: writing part%d (%s)' % (n,part_file)
549
550                        if self.DRY_RUN:
551                                print 'DRY_RUN: NOT saving attachments'
552                                continue
553
554                        fx = open(part_file, 'wb')
555                        text = part.get_payload(decode=1)
556
557                        if not text:
558                                text = '(None)'
559
560                        fx.write(text)
561                        fx.close()
562
563                        try:
564                                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
565                        except OSError:
566                                pass
567
568        def save_email_for_debug(self, message, tempfile=False):
569
570                if tempfile:
571                        import tempfile
572                        msg_file = tempfile.mktemp('.email2trac')
573                else:
574                        #msg_file = '/var/tmp/msg.txt'
575                        msg_file = os.path.join(self.TMPDIR, 'msg.txt')
576
577                if self.DRY_RUN:
578                        print 'DRY_RUN: NOT saving email message to %s' %(msg_file)
579                else:
580                        print 'TD: saving email to %s' %(msg_file)
581
582                        fx = open(msg_file, 'wb')
583                        fx.write('%s' % message)
584                        fx.close()
585                       
586                        try:
587                                os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
588                        except OSError:
589                                pass
590
591                message_parts = self.get_message_parts(message)
592                message_parts = self.unique_attachment_names(message_parts)
593                body_text = self.body_text(message_parts)
594                self.debug_body(body_text, True)
595                self.debug_attachments(message_parts)
596
597########## Conversion functions  ###########################################################
598
599        def str_to_dict(self, s):
600                """
601                Transfrom a string of the form [<key>=<value>]+ to dict[<key>] = <value>
602                """
603
604                fields = string.split(s, self.SUBJECT_FIELD_SEPARATOR)
605
606                result = dict()
607                for field in fields:
608                        try:
609                                index, value = string.split(field, '=')
610
611                                # We can not change the description of a ticket via the subject
612                                # line. The description is the body of the email
613                                #
614                                if index.lower() in ['description']:
615                                        continue
616
617                                if value:
618                                        result[index.lower()] = value
619
620                        except ValueError:
621                                pass
622                return result
623
624########## TRAC ticket functions  ###########################################################
625
626        def update_ticket_fields(self, ticket, user_dict, use_default=None):
627                """
628                This will update the ticket fields. It will check if the
629                given fields are known and if the right values are specified
630                It will only update the ticket field value:
631                        - If the field is known
632                        - If the value supplied is valid for the ticket field.
633                          If not then there are two options:
634                           1) Skip the value (use_default=None)
635                           2) Set default value for field (use_default=1)
636                """
637                if self.VERBOSE:
638                        print "VB: update_ticket_fields"
639
640                # Build a system dictionary from the ticket fields
641                # with field as index and option as value
642                #
643                sys_dict = dict()
644                for field in ticket.fields:
645                        try:
646                                sys_dict[field['name']] = field['options']
647
648                        except KeyError:
649                                sys_dict[field['name']] = None
650                                pass
651
652                ## Check user supplied fields an compare them with the
653                # system one's
654                #
655                for field,value in user_dict.items():
656                        if self.DEBUG >= 10:
657                                print  'user_field\t %s = %s' %(field,value)
658
659                        ## To prevent mail loop
660                        #
661                        if field == 'cc':
662
663                                cc_list = user_dict['cc'].split(',')
664
665                                if self.trac_smtp_from in cc_list:
666                                        if self.DEBUG > 10:
667                                                print 'TD: MAIL LOOP: %s is not allowed as CC address' %(self.trac_smtp_from)
668                                        cc_list.remove(self.trac_smtp_from)
669
670                                value = ','.join(cc_list)
671                               
672
673                        if sys_dict.has_key(field):
674
675                                # Check if value is an allowed system option, if TypeError then
676                                # every value is allowed
677                                #
678                                try:
679                                        if value in sys_dict[field]:
680                                                ticket[field] = value
681                                        else:
682                                                # Must we set a default if value is not allowed
683                                                #
684                                                if use_default:
685                                                        value = self.get_config('ticket', 'default_%s' %(field) )
686                                                        ticket[field] = value
687
688                                except TypeError:
689                                        ticket[field] = value
690
691                                if self.DEBUG >= 10:
692                                        print  'ticket_field\t %s = %s' %(field,  ticket[field])
693                                       
694        def ticket_update(self, m, id, spam):
695                """
696                If the current email is a reply to an existing ticket, this function
697                will append the contents of this email to that ticket, instead of
698                creating a new one.
699                """
700                if self.VERBOSE:
701                        print "VB: ticket_update: %s" %id
702
703                # Must we update ticket fields
704                #
705                update_fields = dict()
706                try:
707                        id, keywords = string.split(id, '?')
708
709                        # Skip the last ':' character
710                        #
711                        keywords = keywords[:-1]
712                        update_fields = self.str_to_dict(keywords)
713
714                        # Strip '#'
715                        #
716                        self.id = int(id[1:])
717
718                except ValueError:
719                        # Strip '#' and ':'
720                        #
721                        self.id = int(id[1:-1])
722
723
724                # When is the change committed
725                #
726                if self.VERSION == 0.11:
727                        utc = UTC()
728                        when = datetime.now(utc)
729                else:
730                        when = int(time.time())
731
732                try:
733                        tkt = Ticket(self.env, self.id, self.db)
734                except util.TracError, detail:
735                        # Not a valid ticket
736                        self.id = None
737                        return False
738
739                # How many changes has this ticket
740                cnum = len(tkt.get_changelog())
741
742
743                # reopen the ticket if it is was closed
744                # We must use the ticket workflow framework
745                #
746                if tkt['status'] in ['closed'] and self.EMAIL_TRIGGERS_WORKFLOW:
747
748                        #print controller.actions['reopen']
749                        #
750                        # As reference 
751                        # req = Mock(href=Href('/'), abs_href=Href('http://www.example.com/'), authname='anonymous', perm=MockPerm(), args={})
752                        #
753                        #a = controller.render_ticket_action_control(req, tkt, 'reopen')
754                        #print 'controller : ', a
755                        #
756                        #b = controller.get_all_status()
757                        #print 'get all status: ', b
758                        #
759                        #b = controller.get_ticket_changes(req, tkt, 'reopen')
760                        #print 'get_ticket_changes :', b
761
762                        if self.WORKFLOW and (self.VERSION in [0.11]) :
763                                from trac.ticket.default_workflow import ConfigurableTicketWorkflow
764                                from trac.test import Mock, MockPerm
765
766                                req = Mock(authname='anonymous', perm=MockPerm(), args={})
767
768                                controller = ConfigurableTicketWorkflow(self.env)
769                                fields = controller.get_ticket_changes(req, tkt, self.WORKFLOW)
770
771                                if self.DEBUG:
772                                        print 'TD: Workflow ticket update fields: ', fields
773
774                                for key in fields.keys():
775                                        tkt[key] = fields[key]
776
777                        else:
778                                tkt['status'] = 'reopened'
779                                tkt['resolution'] = ''
780
781                # Must we update some ticket fields properties via subjectline
782                #
783                if update_fields:
784                        self.update_ticket_fields(tkt, update_fields)
785
786                message_parts = self.get_message_parts(m)
787                message_parts = self.unique_attachment_names(message_parts)
788
789                # Must we update some ticket fields properties via body_text
790                #
791                if self.properties:
792                                self.update_ticket_fields(tkt, self.properties)
793
794                if self.EMAIL_HEADER:
795                        message_parts.insert(0, self.email_header_txt(m))
796
797                body_text = self.body_text(message_parts)
798
799                if body_text.strip() or update_fields or self.properties:
800                        if self.DRY_RUN:
801                                print 'DRY_RUN: tkt.save_changes(self.author, body_text, ticket_change_number) ', self.author, cnum
802                        else:
803                                tkt.save_changes(self.author, body_text, when, None, str(cnum))
804                       
805
806                if self.VERSION  == 0.9:
807                        error_with_attachments = self.attachments(message_parts, True)
808                else:
809                        error_with_attachments = self.attachments(message_parts)
810
811                if self.notification and not spam:
812                        self.notify(tkt, False, when)
813
814                return True
815
816        def set_ticket_fields(self, ticket):
817                """
818                set the ticket fields to value specified
819                        - /etc/email2trac.conf with <prefix>_<field>
820                        - trac default values, trac.ini
821                """
822                user_dict = dict()
823
824                for field in ticket.fields:
825
826                        name = field['name']
827
828                        ## skip some fields like resolution
829                        #
830                        if name in [ 'resolution' ]:
831                                continue
832
833                        ## default trac value
834                        #
835                        if not field.get('custom'):
836                                value = self.get_config('ticket', 'default_%s' %(name) )
837                        else:
838                                value = field.get('value')
839                                options = field.get('options')
840                                if value and options and (value not in options):
841                                        value = options[int(value)]
842
843                        if self.DEBUG > 10:
844                                print 'trac.ini name %s = %s' %(name, value)
845
846                        ## email2trac.conf settings
847                        #
848                        prefix = self.parameters['ticket_prefix']
849                        try:
850                                value = self.parameters['%s_%s' %(prefix, name)]
851                                if self.DEBUG > 10:
852                                        print 'email2trac.conf %s = %s ' %(name, value)
853
854                        except KeyError, detail:
855                                pass
856               
857                        if self.DEBUG:
858                                print 'user_dict[%s] = %s' %(name, value)
859
860                        user_dict[name] = value
861
862                self.update_ticket_fields(ticket, user_dict, use_default=1)
863
864                ## Set status ticket
865                #
866                ticket['status'] = 'new'
867
868
869
870        def new_ticket(self, msg, subject, spam, set_fields = None):
871                """
872                Create a new ticket
873                """
874                if self.DEBUG:
875                        print "TD: new_ticket"
876
877                tkt = Ticket(self.env)
878
879                self.set_reply_fields(tkt, msg)
880
881                self.set_ticket_fields(tkt)
882
883                # Old style setting for component, will be removed
884                #
885                if spam:
886                        tkt['component'] = 'Spam'
887
888                elif self.parameters.has_key('component'):
889                        tkt['component'] = self.parameters['component']
890
891                if not msg['Subject']:
892                        tkt['summary'] = u'(No subject)'
893                else:
894                        tkt['summary'] = subject
895
896
897                if set_fields:
898                        rest, keywords = string.split(set_fields, '?')
899
900                        if keywords:
901                                update_fields = self.str_to_dict(keywords)
902                                self.update_ticket_fields(tkt, update_fields)
903
904                # produce e-mail like header
905                #
906                head = ''
907                if self.EMAIL_HEADER > 0:
908                        head = self.email_header_txt(msg)
909
910                message_parts = self.get_message_parts(msg)
911
912                # Must we update some ticket fields properties via body_text
913                #
914                if self.properties:
915                                self.update_ticket_fields(tkt, self.properties)
916
917                if self.DEBUG:
918                        print 'TD: self.get_message_parts ',
919                        print message_parts
920
921                message_parts = self.unique_attachment_names(message_parts)
922                if self.DEBUG:
923                        print 'TD: self.unique_attachment_names',
924                        print message_parts
925               
926                if self.EMAIL_HEADER > 0:
927                        message_parts.insert(0, self.email_header_txt(msg))
928                       
929                body_text = self.body_text(message_parts)
930
931                tkt['description'] = body_text
932
933                #when = int(time.time())
934                #
935                utc = UTC()
936                when = datetime.now(utc)
937
938                if not self.DRY_RUN:
939                        self.id = tkt.insert()
940       
941                changed = False
942                comment = ''
943
944                # some routines in trac are dependend on ticket id     
945                # like alternate notify template
946                #
947                if self.notify_template:
948                        tkt['id'] = self.id
949                        changed = True
950
951                ## Rewrite the description if we have mailto enabled
952                #
953                if self.MAILTO:
954                        changed = True
955                        comment = u'\nadded mailto line\n'
956                        mailto = self.html_mailto_link( m['Subject'], body_text)
957
958                        tkt['description'] = u'%s\r\n%s%s\r\n' \
959                                %(head, mailto, body_text)
960       
961                ## Save the attachments to the ticket   
962                #
963                error_with_attachments =  self.attachments(message_parts)
964
965                if error_with_attachments:
966                        changed = True
967                        comment = '%s\n%s\n' %(comment, error_with_attachments)
968
969                if changed:
970                        if self.DRY_RUN:
971                                print 'DRY_RUN: tkt.save_changes(self.author, comment) ', self.author
972                        else:
973                                tkt.save_changes(self.author, comment)
974                                #print tkt.get_changelog(self.db, when)
975
976                if self.notification and not spam:
977                        self.notify(tkt, True)
978
979
980########## Fullblog functions  ###########################################################
981
982        def blog(self, id):
983                """
984                The blog create/update function
985                """
986                # import the modules
987                #
988                from tracfullblog.core import FullBlogCore
989                from tracfullblog.model import BlogPost, BlogComment
990                from trac.test import Mock, MockPerm
991
992                # instantiate blog core
993                blog = FullBlogCore(self.env)
994                req = Mock(authname='anonymous', perm=MockPerm(), args={})
995
996                if id:
997
998                        # update blog
999                        #
1000                        comment = BlogComment(self.env, id)
1001                        comment.author = self.author
1002
1003                        message_parts = self.get_message_parts(m)
1004                        comment.comment = self.body_text(message_parts)
1005
1006                        blog.create_comment(req, comment)
1007
1008                else:
1009                        # create blog
1010                        #
1011                        import time
1012                        post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime()))
1013
1014                        #post = BlogPost(self.env, blog._get_default_postname(self.env))
1015                       
1016                        post.author = self.author
1017                        post.title = self.email_to_unicode(m['Subject'])
1018
1019                        message_parts = self.get_message_parts(m)
1020                        post.body = self.body_text(message_parts)
1021                       
1022                        blog.create_post(req, post, self.author, u'Created by email2trac', False)
1023
1024
1025        def parse(self, fp):
1026                global m
1027
1028                m = email.message_from_file(fp)
1029               
1030                if not m:
1031                        if self.DEBUG:
1032                                print "TD: This is not a valid email message format"
1033                        return
1034                       
1035                # Work around lack of header folding in Python; see http://bugs.python.org/issue4696
1036                try:
1037                        m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
1038                except AttributeError, detail:
1039                        pass
1040
1041                if self.DEBUG > 1:        # save the entire e-mail message text
1042                        self.save_email_for_debug(m, True)
1043
1044                self.db = self.env.get_db_cnx()
1045                self.get_sender_info(m)
1046
1047                if not self.email_header_acl('white_list', self.email_addr, True):
1048                        if self.DEBUG > 1 :
1049                                print 'Message rejected : %s not in white list' %(self.email_addr)
1050                        return False
1051
1052                if self.email_header_acl('black_list', self.email_addr, False):
1053                        if self.DEBUG > 1 :
1054                                print 'Message rejected : %s in black list' %(self.email_addr)
1055                        return False
1056
1057                if not self.email_header_acl('recipient_list', self.to_email_addr, True):
1058                        if self.DEBUG > 1 :
1059                                print 'Message rejected : %s not in recipient list' %(self.to_email_addr)
1060                        return False
1061
1062                # If drop the message
1063                #
1064                if self.spam(m) == 'drop':
1065                        return False
1066
1067                elif self.spam(m) == 'spam':
1068                        spam_msg = True
1069                else:
1070                        spam_msg = False
1071
1072                if self.get_config('notification', 'smtp_enabled') in ['true']:
1073                        self.notification = 1
1074                else:
1075                        self.notification = 0
1076
1077
1078                # Check if  FullBlogPlugin is installed
1079                #
1080                blog_enabled = None
1081                if self.get_config('components', 'tracfullblog.*') in ['enabled']:
1082                        blog_enabled = True
1083
1084                if not m['Subject']:
1085                        subject  = 'No Subject'
1086                else:
1087                        subject  = self.email_to_unicode(m['Subject'])         
1088
1089                #
1090                # [hic] #1529: Re: LRZ
1091                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
1092                #
1093                TICKET_RE = re.compile(r"""
1094                        (?P<blog>blog:(?P<blog_id>\w*))
1095                        |(?P<new_fields>[#][?].*)
1096                        |(?P<reply>[#][\d]+:)
1097                        |(?P<reply_fields>[#][\d]+\?.*?:)
1098                        """, re.VERBOSE)
1099
1100                # Find out if this is a ticket or a blog
1101                #
1102                result =  TICKET_RE.search(subject)
1103
1104                if result:
1105                        if result.group('blog'):
1106                                if blog_enabled:
1107                                        self.blog(result.group('blog_id'))
1108                                else:
1109                                        if self.DEBUG:
1110                                                print "Fullblog plugin is not installed"
1111                                                return
1112
1113                        # update ticket + fields
1114                        #
1115                        if result.group('reply_fields') and self.TICKET_UPDATE:
1116                                self.ticket_update(m, result.group('reply_fields'), spam_msg)
1117
1118                        # Update ticket
1119                        #
1120                        elif result.group('reply') and self.TICKET_UPDATE:
1121                                self.ticket_update(m, result.group('reply'), spam_msg)
1122
1123                        # New ticket + fields
1124                        #
1125                        elif result.group('new_fields'):
1126                                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
1127
1128                # Create ticket
1129                #
1130                else:
1131                        self.new_ticket(m, subject, spam_msg)
1132
1133        def strip_signature(self, text):
1134                """
1135                Strip signature from message, inspired by Mailman software
1136                """
1137                body = []
1138                for line in text.splitlines():
1139                        if line == '-- ':
1140                                break
1141                        body.append(line)
1142
1143                return ('\n'.join(body))
1144
1145        def reflow(self, text, delsp = 0):
1146                """
1147                Reflow the message based on the format="flowed" specification (RFC 3676)
1148                """
1149                flowedlines = []
1150                quotelevel = 0
1151                prevflowed = 0
1152
1153                for line in text.splitlines():
1154                        from re import match
1155                       
1156                        # Figure out the quote level and the content of the current line
1157                        m = match('(>*)( ?)(.*)', line)
1158                        linequotelevel = len(m.group(1))
1159                        line = m.group(3)
1160
1161                        # Determine whether this line is flowed
1162                        if line and line != '-- ' and line[-1] == ' ':
1163                                flowed = 1
1164                        else:
1165                                flowed = 0
1166
1167                        if flowed and delsp and line and line[-1] == ' ':
1168                                line = line[:-1]
1169
1170                        # If the previous line is flowed, append this line to it
1171                        if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1172                                flowedlines[-1] += line
1173                        # Otherwise, start a new line
1174                        else:
1175                                flowedlines.append('>' * linequotelevel + line)
1176
1177                        prevflowed = flowed
1178                       
1179
1180                return '\n'.join(flowedlines)
1181
1182        def strip_quotes(self, text):
1183                """
1184                Strip quotes from message by Nicolas Mendoza
1185                """
1186                body = []
1187                for line in text.splitlines():
1188                        if line.startswith(self.EMAIL_QUOTE):
1189                                continue
1190                        body.append(line)
1191
1192                return ('\n'.join(body))
1193
1194        def inline_properties(self, text):
1195                """
1196                Parse text if we use inline keywords to set ticket fields
1197                """
1198                if self.DEBUG:
1199                        print 'TD: inline_properties function'
1200
1201                properties = dict()
1202                body = list()
1203
1204                INLINE_EXP = re.compile('\s*[@]\s*([a-zA-Z]+)\s*:(.*)$')
1205
1206                for line in text.splitlines():
1207                        match = INLINE_EXP.match(line)
1208                        if match:
1209                                keyword, value = match.groups()
1210                                self.properties[keyword] = value.strip()
1211                                if self.DEBUG:
1212                                        print "TD: inline properties: %s : %s" %(keyword,value)
1213                        else:
1214                                body.append(line)
1215                               
1216                return '\n'.join(body)
1217
1218
1219        def wrap_text(self, text, replace_whitespace = False):
1220                """
1221                Will break a lines longer then given length into several small
1222                lines of size given length
1223                """
1224                import textwrap
1225
1226                LINESEPARATOR = '\n'
1227                reformat = ''
1228
1229                for s in text.split(LINESEPARATOR):
1230                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
1231                        if tmp:
1232                                reformat = '%s\n%s' %(reformat,tmp)
1233                        else:
1234                                reformat = '%s\n' %reformat
1235
1236                return reformat
1237
1238                # Python2.4 and higher
1239                #
1240                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1241                #
1242
1243
1244        def get_message_parts(self, msg):
1245                """
1246                parses the email message and returns a list of body parts and attachments
1247                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
1248                """
1249                if self.VERBOSE:
1250                        print "VB: get_message_parts()"
1251
1252                message_parts = list()
1253       
1254                ALTERNATIVE_MULTIPART = False
1255
1256                for part in msg.walk():
1257                        if self.DEBUG:
1258                                print 'TD: Message part: Main-Type: %s' % part.get_content_maintype()
1259                                print 'TD: Message part: Content-Type: %s' % part.get_content_type()
1260
1261                        ## Check content type
1262                        #
1263                        if part.get_content_type() in self.STRIP_CONTENT_TYPES:
1264
1265                                if self.DEBUG:
1266                                        print "TD: A %s attachment named '%s' was skipped" %(part.get_content_type(), part.get_filename())
1267
1268                                continue
1269
1270                        ## Catch some mulitpart execptions
1271                        #
1272                        if part.get_content_type() == 'multipart/alternative':
1273                                ALTERNATIVE_MULTIPART = True
1274                                continue
1275
1276                        ## Skip multipart containers
1277                        #
1278                        if part.get_content_maintype() == 'multipart':
1279                                if self.DEBUG:
1280                                        print "TD: Skipping multipart container"
1281                                continue
1282                       
1283                        ## Check if this is an inline part. It's inline if there is co Cont-Disp header, or if there is one and it says "inline"
1284                        #
1285                        inline = self.inline_part(part)
1286
1287                        ## Drop HTML message
1288                        #
1289                        if ALTERNATIVE_MULTIPART and self.DROP_ALTERNATIVE_HTML_VERSION:
1290                                if part.get_content_type() == 'text/html':
1291                                        if self.DEBUG:
1292                                                print "TD: Skipping alternative HTML message"
1293
1294                                        ALTERNATIVE_MULTIPART = False
1295                                        continue
1296
1297                        ## Inline text parts are where the body is
1298                        #
1299                        if part.get_content_type() == 'text/plain' and inline:
1300                                if self.DEBUG:
1301                                        print 'TD:               Inline body part'
1302
1303                                # Try to decode, if fails then do not decode
1304                                #
1305                                body_text = part.get_payload(decode=1)
1306                                if not body_text:                       
1307                                        body_text = part.get_payload(decode=0)
1308
1309                                format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1310                                delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
1311
1312                                if self.REFLOW and not self.VERBATIM_FORMAT and format == 'flowed':
1313                                        body_text = self.reflow(body_text, delsp == 'yes')
1314       
1315                                if self.STRIP_SIGNATURE:
1316                                        body_text = self.strip_signature(body_text)
1317
1318                                if self.STRIP_QUOTES:
1319                                        body_text = self.strip_quotes(body_text)
1320
1321                                if self.INLINE_PROPERTIES:
1322                                        body_text = self.inline_properties(body_text)
1323
1324                                if self.USE_TEXTWRAP:
1325                                        body_text = self.wrap_text(body_text)
1326
1327                                ## Get contents charset (iso-8859-15 if not defined in mail headers)
1328                                #
1329                                charset = part.get_content_charset()
1330                                if not charset:
1331                                        charset = 'iso-8859-15'
1332
1333                                try:
1334                                        ubody_text = unicode(body_text, charset)
1335
1336                                except UnicodeError, detail:
1337                                        ubody_text = unicode(body_text, 'iso-8859-15')
1338
1339                                except LookupError, detail:
1340                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
1341
1342                                if self.VERBATIM_FORMAT:
1343                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1344                                else:
1345                                        message_parts.append('%s' %ubody_text)
1346                        else:
1347                                if self.DEBUG:
1348                                        try:
1349                                                print 'TD:               Filename: %s' % part.get_filename()
1350                                        except UnicodeEncodeError, detail:
1351                                                print 'TD:               Filename: Can not be printed due to non-ascii characters'
1352
1353                                ## Convert 7-bit filename to 8 bits value
1354                                #
1355                                filename = self.email_to_unicode(part.get_filename())
1356                                message_parts.append((filename, part))
1357
1358                return message_parts
1359               
1360        def unique_attachment_names(self, message_parts):
1361                """
1362                """
1363                renamed_parts = []
1364                attachment_names = set()
1365
1366                for item in message_parts:
1367                       
1368                        ## If not an attachment, leave it alone
1369                        #
1370                        if not isinstance(item, tuple):
1371                                renamed_parts.append(item)
1372                                continue
1373                               
1374                        (filename, part) = item
1375
1376                        ## If no filename, use a default one
1377                        #
1378                        if not filename:
1379                                filename = 'untitled-part'
1380
1381                                # Guess the extension from the content type, use non strict mode
1382                                # some additional non-standard but commonly used MIME types
1383                                # are also recognized
1384                                #
1385                                ext = mimetypes.guess_extension(part.get_content_type(), False)
1386                                if not ext:
1387                                        ext = '.bin'
1388
1389                                filename = '%s%s' % (filename, ext)
1390
1391# We now use the attachment insert function
1392#
1393                        ## Discard relative paths in attachment names
1394                        #
1395                        #filename = filename.replace('\\', '/').replace(':', '/')
1396                        #filename = os.path.basename(filename)
1397                        #
1398                        # We try to normalize the filename to utf-8 NFC if we can.
1399                        # Files uploaded from OS X might be in NFD.
1400                        # Check python version and then try it
1401                        #
1402                        #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
1403                        #       try:
1404                        #               filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
1405                        #       except TypeError:
1406                        #               pass
1407
1408                        # Make the filename unique for this ticket
1409                        num = 0
1410                        unique_filename = filename
1411                        dummy_filename, ext = os.path.splitext(filename)
1412
1413                        while (unique_filename in attachment_names) or self.attachment_exists(unique_filename):
1414                                num += 1
1415                                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
1416                               
1417                        if self.DEBUG:
1418                                try:
1419                                        print 'TD: Attachment with filename %s will be saved as %s' % (filename, unique_filename)
1420                                except UnicodeEncodeError, detail:
1421                                        print 'Filename can not be printed due to non-ascii characters'
1422
1423                        attachment_names.add(unique_filename)
1424
1425                        renamed_parts.append((filename, unique_filename, part))
1426       
1427                return renamed_parts
1428                       
1429        def inline_part(self, part):
1430                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1431               
1432                       
1433        def attachment_exists(self, filename):
1434
1435                if self.DEBUG:
1436                        s = 'TD: attachment already exists: Ticket id : '
1437                        try:
1438                                print "%s%s, Filename : %s" %(s, self.id, filename)
1439                        except UnicodeEncodeError, detail:
1440                                print "%s%s, Filename : Can not be printed due to non-ascii characters" %(s, self.id)
1441
1442                # We have no valid ticket id
1443                #
1444                if not self.id:
1445                        return False
1446
1447                try:
1448                        att = attachment.Attachment(self.env, 'ticket', self.id, filename)
1449                        return True
1450                except attachment.ResourceNotFound:
1451                        return False
1452                       
1453        def body_text(self, message_parts):
1454                body_text = []
1455               
1456                for part in message_parts:
1457                        # Plain text part, append it
1458                        if not isinstance(part, tuple):
1459                                body_text.extend(part.strip().splitlines())
1460                                body_text.append("")
1461                                continue
1462                               
1463                        (original, filename, part) = part
1464                        inline = self.inline_part(part)
1465                       
1466                        if part.get_content_maintype() == 'image' and inline:
1467                                body_text.append('[[Image(%s)]]' % filename)
1468                                body_text.append("")
1469                        else:
1470                                body_text.append('[attachment:"%s"]' % filename)
1471                                body_text.append("")
1472                               
1473                body_text = '\r\n'.join(body_text)
1474                return body_text
1475
1476        def notify(self, tkt, new=True, modtime=0):
1477                """
1478                A wrapper for the TRAC notify function. So we can use templates
1479                """
1480                if self.DRY_RUN:
1481                                print 'DRY_RUN: self.notify(tkt, True) ', self.author
1482                                return
1483                try:
1484                        # create false {abs_}href properties, to trick Notify()
1485                        #
1486                        if not self.VERSION == 0.11:
1487                                self.env.abs_href = Href(self.get_config('project', 'url'))
1488                                self.env.href = Href(self.get_config('project', 'url'))
1489
1490                        tn = TicketNotifyEmail(self.env)
1491
1492                        if self.notify_template:
1493
1494                                if self.VERSION == 0.11:
1495
1496                                        from trac.web.chrome import Chrome
1497
1498                                        if self.notify_template_update and not new:
1499                                                tn.template_name = self.notify_template_update
1500                                        else:
1501                                                tn.template_name = self.notify_template
1502
1503                                        tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
1504                                               
1505                                else:
1506
1507                                        tn.template_name = self.notify_template;
1508
1509                        tn.notify(tkt, new, modtime)
1510
1511                except Exception, e:
1512                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(self.id, e)
1513
1514        def html_mailto_link(self, subject, body):
1515                """
1516                This function returns a HTML mailto tag with the ticket id and author email address
1517                """
1518                if not self.author:
1519                        author = self.email_addr
1520                else:   
1521                        author = self.author
1522
1523                # use urllib to escape the chars
1524                #
1525                s = 'mailto:%s?Subject=%s&Cc=%s' %(
1526                       urllib.quote(self.email_addr),
1527                           urllib.quote('Re: #%s: %s' %(self.id, subject)),
1528                           urllib.quote(self.MAILTO_CC)
1529                           )
1530
1531                s = '\r\n{{{\r\n#!html\r\n<a\r\n href="%s">Reply to: %s\r\n</a>\r\n}}}\r\n' %(s, author)
1532                return s
1533
1534        def attachments(self, message_parts, update=False):
1535                '''
1536                save any attachments as files in the ticket's directory
1537                '''
1538                if self.DRY_RUN:
1539                        print "DRY_RUN: no attachments saved"
1540                        return ''
1541
1542                count = 0
1543
1544                # Get Maxium attachment size
1545                #
1546                max_size = int(self.get_config('attachment', 'max_size'))
1547                status   = None
1548               
1549                for item in message_parts:
1550                        # Skip body parts
1551                        if not isinstance(item, tuple):
1552                                continue
1553                               
1554                        (original, filename, part) = item
1555                        #
1556                        # Must be tuneables HvB
1557                        #
1558                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, filename))
1559                        text = part.get_payload(decode=1)
1560                        if not text:
1561                                text = '(None)'
1562                        fd.write(text)
1563                        fd.close()
1564
1565                        # get the file_size
1566                        #
1567                        stats = os.lstat(path)
1568                        file_size = stats[stat.ST_SIZE]
1569
1570                        # Check if the attachment size is allowed
1571                        #
1572                        if (max_size != -1) and (file_size > max_size):
1573                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1574                                        %(status, original, file_size, max_size)
1575
1576                                os.unlink(path)
1577                                continue
1578                        else:
1579                                count = count + 1
1580                                       
1581                        # Insert the attachment
1582                        #
1583                        fd = open(path, 'rb')
1584                        att = attachment.Attachment(self.env, 'ticket', self.id)
1585
1586                        # This will break the ticket_update system, the body_text is vaporized
1587                        # ;-(
1588                        #
1589                        if not update:
1590                                att.author = self.author
1591                                att.description = self.email_to_unicode('Added by email2trac')
1592
1593                        att.insert(filename, fd, file_size)
1594
1595                        #except  util.TracError, detail:
1596                        #       print detail
1597
1598                        # Remove the created temporary filename
1599                        #
1600                        fd.close()
1601                        os.unlink(path)
1602
1603                ## return error
1604                #
1605                return status
1606
1607
1608def ReadConfig(file, name):
1609        """
1610        Parse the config file
1611        """
1612        if not os.path.isfile(file):
1613                print 'File %s does not exist' %file
1614                sys.exit(1)
1615
1616        config = trac_config.Configuration(file)
1617
1618        # Use given project name else use defaults
1619        #
1620        if name:
1621                sections = config.sections()
1622                if not name in sections:
1623                        print "Not a valid project name: %s" %name
1624                        print "Valid names: %s" %sections
1625                        sys.exit(1)
1626
1627                project =  dict()
1628                for option, value in  config.options(name):
1629                        project[option] = value
1630
1631        else:
1632                # use some trac internals to get the defaults
1633                #
1634                project = config.parser.defaults()
1635
1636        return project
1637
1638
1639if __name__ == '__main__':
1640        # Default config file
1641        #
1642        configfile = '@email2trac_conf@'
1643        project = ''
1644        component = ''
1645        ticket_prefix = 'default'
1646        dry_run = None
1647        verbose = None
1648
1649        ENABLE_SYSLOG = 0
1650
1651
1652        SHORT_OPT = 'chf:np:t:v'
1653        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'verbose']
1654
1655        try:
1656                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
1657        except getopt.error,detail:
1658                print __doc__
1659                print detail
1660                sys.exit(1)
1661       
1662        project_name = None
1663        for opt,value in opts:
1664                if opt in [ '-h', '--help']:
1665                        print __doc__
1666                        sys.exit(0)
1667                elif opt in ['-c', '--component']:
1668                        component = value
1669                elif opt in ['-f', '--file']:
1670                        configfile = value
1671                elif opt in ['-n', '--dry-run']:
1672                        dry_run = True
1673                elif opt in ['-p', '--project']:
1674                        project_name = value
1675                elif opt in ['-t', '--ticket_prefix']:
1676                        ticket_prefix = value
1677                elif opt in ['-v', '--version']:
1678                        verbose = True
1679       
1680        settings = ReadConfig(configfile, project_name)
1681        if not settings.has_key('project'):
1682                print __doc__
1683                print 'No Trac project is defined in the email2trac config file.'
1684                sys.exit(1)
1685       
1686        if component:
1687                settings['component'] = component
1688
1689        # The default prefix for ticket values in email2trac.conf
1690        #
1691        settings['ticket_prefix'] = ticket_prefix
1692        settings['dry_run'] = dry_run
1693        settings['verbose'] = verbose
1694       
1695        if settings.has_key('trac_version'):
1696                version = settings['trac_version']
1697        else:
1698                version = trac_default_version
1699
1700
1701        #debug HvB
1702        #print settings
1703
1704        try:
1705                if version == '0.9':
1706                        from trac import attachment
1707                        from trac.env import Environment
1708                        from trac.ticket import Ticket
1709                        from trac.web.href import Href
1710                        from trac import util
1711                        from trac.Notify import TicketNotifyEmail
1712                elif version == '0.10':
1713                        from trac import attachment
1714                        from trac.env import Environment
1715                        from trac.ticket import Ticket
1716                        from trac.web.href import Href
1717                        from trac import util
1718                        #
1719                        # return  util.text.to_unicode(str)
1720                        #
1721                        # see http://projects.edgewall.com/trac/changeset/2799
1722                        from trac.ticket.notification import TicketNotifyEmail
1723                        from trac import config as trac_config
1724                elif version == '0.11':
1725                        from trac import attachment
1726                        from trac.env import Environment
1727                        from trac.ticket import Ticket
1728                        from trac.web.href import Href
1729                        from trac import config as trac_config
1730                        from trac import util
1731
1732
1733                        #
1734                        # return  util.text.to_unicode(str)
1735                        #
1736                        # see http://projects.edgewall.com/trac/changeset/2799
1737                        from trac.ticket.notification import TicketNotifyEmail
1738                else:
1739                        print 'TRAC version %s is not supported' %version
1740                        sys.exit(1)
1741                       
1742                if settings.has_key('enable_syslog'):
1743                        if SYSLOG_AVAILABLE:
1744                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
1745
1746
1747                # Must be set before environment is created
1748                #
1749                if settings.has_key('python_egg_cache'):
1750                        python_egg_cache = str(settings['python_egg_cache'])
1751                        os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
1752
1753                env = Environment(settings['project'], create=0)
1754
1755                tktparser = TicketEmailParser(env, settings, float(version))
1756                tktparser.parse(sys.stdin)
1757
1758        # Catch all errors ans log to SYSLOG if we have enabled this
1759        # else stdout
1760        #
1761        except Exception, error:
1762                if ENABLE_SYSLOG:
1763                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
1764
1765                        etype, evalue, etb = sys.exc_info()
1766                        for e in traceback.format_exception(etype, evalue, etb):
1767                                syslog.syslog(e)
1768
1769                        syslog.closelog()
1770                else:
1771                        traceback.print_exc()
1772
1773                if m:
1774                        tktparser.save_email_for_debug(m, True)
1775
1776                sys.exit(1)
1777# EOB
Note: See TracBrowser for help on using the repository browser.