Main function refactored and connection failure exception handling added
[imap-fix-internaldate] / fix_imap_internaldate.py
1 '''
2 fix_imap_internaldate.py - Fix the INTERNALDATE field on IMAP servers
3
4 Copyright (c) 2012 Intra2net AG
5 Author: Plamen Dimitrov
6
7 This program is free software: you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation, either version 3 of the License, or
10 (at your option) any later version.
11
12 This program is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 GNU General Public License for more details.
16 '''
17
18 import csv
19 import argparse, configparser
20 import logging
21 from mail_date_parser import MailDateParser
22 from mail_iterator import MailIterator
23 from caching_data import CachingData
24
25 def main():
26     """Interprets command arguments and initializes configuration and logger.
27         Then begins mail synchronization."""
28
29     # parse arguments
30     parser = argparse.ArgumentParser(description="Fix the INTERNALDATE field on IMAP servers. "
31                                                 "Small tool to fix the IMAP internaldate "
32                                                 "in case it's too much off compared to the last date "
33                                                 "stored in the received lines.")
34     parser.add_argument('-u', '--update', dest='test_mode', action='store_false',
35                         default=True, help='update all e-mails and exit test mode')
36
37     # config and logging setup
38     config = load_configuration()
39     prepare_logger(config)
40     args = parser.parse_args()
41     if(args.test_mode):
42         logging.info("Testing mode initiated. No message will be modified on the server.")
43     else:
44         logging.info("Update mode initiated. Messages will be modified.")
45
46     # proceed to main functionality
47     synchronize_csv(config, args.test_mode)
48
49     return
50
51 def load_configuration():
52     """Loads the script configuration from a file or creates such."""
53     config = configparser.RawConfigParser()    
54     success = config.read('confscript.cfg')
55     if(len(success)==0):
56         config.add_section('basic_settings')
57         config.set('basic_settings', 'file_log_level', logging.INFO)
58         config.set('basic_settings', 'console_log_level', logging.INFO)
59         config.set('basic_settings', 'imap_server', 'imap.company.com')
60         config.set('basic_settings', 'tolerance', 30)
61         with open('confscript.cfg', 'w') as configfile:
62             config.write(configfile)
63     return config
64
65 def prepare_logger(config):
66     """Sets up the logging functionality"""
67     
68     # reset the log
69     with open('fix_imap_internaldate.log', 'w'):
70         pass
71     
72     # add basic configuration
73     logging.basicConfig(filename='fix_imap_internaldate.log',
74                         format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
75                         level=config.getint('basic_settings', 'file_log_level'))
76     
77     # add a handler for a console output
78     console = logging.StreamHandler()
79     console.setLevel(config.getint('basic_settings', 'console_log_level'))
80     formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
81     console.setFormatter(formatter)
82     logging.getLogger('').addHandler(console)
83     return
84
85 def synchronize_csv(config, test_mode):
86     """Iterates through csv list of users and synchronizes their messages."""
87
88     # initialize loop permanent data
89     caching_data = CachingData()  
90     date_parser = MailDateParser()
91     server = config.get('basic_settings', 'imap_server')
92     tolerance = config.getint('basic_settings', 'tolerance') * 60
93
94     # iterate through the users in the csv data
95     user_reader = csv.DictReader(open("userdata.csv", "r"), delimiter=',')
96     for user in user_reader:
97         try:
98             session = MailIterator(server, user['username'], user['password'])
99         except UserWarning as ex:
100             logging.error(ex)
101             continue
102         for mailbox in session:
103             try:
104                 box = caching_data.retrieve_cached_mailbox(mailbox[0], mailbox[1], user['username'])
105                 mail_ids = session.fetch_messages()
106                 new_ids = box.synchronize(mail_ids, tolerance)
107                 logging.info("%s new messages out of %s found in %s.", len(new_ids), len(mail_ids), box.name)
108             except UserWarning as ex:
109                 logging.error(ex)
110                 continue
111             for mid in new_ids:
112                 try:
113                     fetched_internal_date = session.fetch_internal_date(mid)
114                     internal_date = date_parser.extract_internal_date(fetched_internal_date)
115                     fetched_received_date = session.fetch_received_date(mid)
116                     received_date = date_parser.extract_received_date(fetched_received_date)
117                     if(received_date==""):
118                         logging.debug("No received date could be found in message uid: %s - mailbox: %s - user: %s.",
119                                         mid.decode('iso-8859-1'), box.name, box.owner)
120                         box.no_received_field += 1
121                         continue
122                 except UserWarning as ex:
123                     logging.error(ex)
124                     continue
125                 if(date_parser.compare_dates(received_date, internal_date, tolerance)):
126                     logging.warning("Date conflict found in message uid: %s - mailbox: %s - user: %s.\nInternal date %s is different from received date %s from RECEIVED header:\n%s.",
127                                     mid.decode('iso-8859-1'), box.name, box.owner,
128                                     internal_date.strftime("%d %b %Y %H:%M:%S"),
129                                     received_date.strftime("%d %b %Y %H:%M:%S"),
130                                     fetched_received_date.split("Received:")[1])
131                     if(test_mode==0):
132                         try:
133                             session.update_message(mid, box.name, received_date)
134                         except UserWarning as ex:
135                             logging.error(ex)
136                             continue
137
138                     # count total emails for every user and mailbox
139                     box.date_conflicts += 1
140
141             # if all messages were successfully fixed confirm caching
142             if(not test_mode):
143                 box.confirm_change()
144     
145         # final report on date conflicts
146         caching_data.report_conflicts()
147     return
148
149 if(__name__ == "__main__"):
150     main()