4 # Copyright 2015, Olivier MATZ <zer0@droids-corp.org>
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions are met:
9 # * Redistributions of source code must retain the above copyright
10 # notice, this list of conditions and the following disclaimer.
11 # * Redistributions in binary form must reproduce the above copyright
12 # notice, this list of conditions and the following disclaimer in the
13 # documentation and/or other materials provided with the distribution.
14 # * Neither the name of the University of California, Berkeley nor the
15 # names of its contributors may be used to endorse or promote products
16 # derived from this software without specific prior written permission.
18 # THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
19 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 # DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
22 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25 # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 import imapami.actions
31 import imapami.conditions
44 _LOGLEVELS = [logging.CRITICAL, logging.ERROR, logging.WARNING,
45 logging.INFO, logging.DEBUG]
47 class Imapami(object):
49 The yaml configuration is a collection (dictionary). The following keys
52 - server: <string> [mandatory]
53 Hostname or IP address of the IMAP server to connect to.
55 - port: <integer> [optional]
56 The TCP port to connect to. If not specified, use the default,
57 depending on whether SSL is enabled or disabled.
59 - ssl: <boolean> [optional, default is True]
60 Enable or disable SSL (True or False).
62 - login: <string> [optional]
63 IMAP login name. If not specified, the login is asked on stdin.
65 - password: <string> [optional]
66 IMAP password. If not specified, the password is asked on stdin.
68 - logfile: <string> [optional]
69 File where messages are logged. If not specified, no logs are
72 - loglevel: <integer> [optional]
73 Level of logs written in logfile. Possible value are from 0 (no log)
74 to 4 (debug). Default value is 3 (info).
76 - inbox: <string> [optional, default is INBOX]
77 Default mailbox directory where rules get message from. It can be
78 overriden inside the rule.
80 - rules: <list of rules> [mandatory]
81 The list of rules to be applied. See the rule syntax for details.
83 Any key entry that is not reserved for a configuration parameter is
84 saved as a variable. The variables can be used in rules by referencing
85 them as {my_var} in conditions or actions arguments.
87 Variables are text only, it is possible to test them in rule conditions
88 and set them in rule actions.
91 def __init__(self, config, loglevel=3):
93 Create a Imapami object.
96 The yaml configuration.
97 :arg integer loglevel:
98 The level of the console logs.
101 self.config = {"server": None,
112 self.logger = self._get_logger(loglevel)
113 self._load_config(config)
114 self._update_logger()
117 def _get_logger(self, loglevel):
119 Create a logger, and configure it to log on console. This is done
120 before configuration is parsed.
122 :arg integer loglevel:
123 The level of the console logs from 0 (critical) to 4 (debug).
126 if not isinstance(loglevel, int) or loglevel < 0 or loglevel > 4:
127 sys.stderr.write("Invalid log level\n")
130 # create a logger and a stream handler on console
131 logger = logging.getLogger('imapami')
132 logger.setLevel(logging.DEBUG)
133 console_handler = logging.StreamHandler()
134 console_handler.setLevel(_LOGLEVELS[loglevel])
135 formatter = logging.Formatter('%(levelname)s - %(message)s')
136 console_handler.setFormatter(formatter)
137 logger.addHandler(console_handler)
138 self.console_handler = console_handler
141 def _update_logger(self):
143 Update the logger used after the configuration is parsed: add
144 the specified log file in addition to console.
146 logfile = self.config.get("logfile")
147 # continue to log on console
150 # else, add the new file handler
151 file_handler = logging.FileHandler(logfile)
152 loglevel = self.config.get("loglevel")
153 file_handler.setLevel(_LOGLEVELS[loglevel])
154 formatter = logging.Formatter(
155 '%(asctime)s - %(levelname)s - %(message)s')
156 file_handler.setFormatter(formatter)
157 self.logger.addHandler(file_handler)
159 def _load_config(self, config):
161 Load the yaml configuration. It parses the config parameters, the
162 variables, and the list of rules.
165 The yaml configuration.
167 yaml_dict = yaml.safe_load(config)
168 for key in yaml_dict:
169 if key in self.config:
170 self.config[key] = yaml_dict[key]
171 self.logger.debug("set config %s = %s", key, yaml_dict[key])
173 self.variables[key] = yaml_dict[key]
174 self.logger.debug("set variable %s = %s", key, yaml_dict[key])
175 for key in ["server", "ssl", "rules"]:
176 if self.config[key] is None:
177 self.logger.error("%s is not specified in configuration")
179 rules = self.config["rules"]
181 self.logger.error("no rule defined")
183 for i, rule in enumerate(rules):
185 self.rules.append(imapami.rules.new(rule))
186 except Exception as e:
187 self.logger.error("error while processing rule %d: %s",
188 i+1, rule.get('name', 'no-name'))
193 Connect and login to the remote IMAP server.
195 if self.config.get("ssl") == True:
196 imap_class = imaplib.IMAP4_SSL
198 imap_class = imaplib.IMAP4
200 login = self.config.get("login")
202 login = raw_input("Username: ")
203 password = self.config.get("password")
205 password = getpass.getpass()
206 server = self.config.get("server")
207 port = self.config.get("port")
209 imap = imap_class(server)
211 imap = imap_class(server, port)
212 self.logger.info('Connecting to %s@%s...', login, server)
213 imap.login(login, password)
216 def get_uidnext(self):
218 Get the state (uidnext) for each inbox used in the configuration.
219 It gives the uid of the next message the will be added in the mbox.
220 We will only care about messages with a uid lower than this uidnext,
221 to avoid race conditions with a message arriving while we are in the
222 middle of rules processing.
224 self.logger.info('Getting inbox state...')
225 mboxes = [self.config["inbox"]] + [rule.inbox for rule in self.rules]
229 if self.uidnext.get(m, None) is not None:
232 typ, dat = self.imap.status(m, "(UIDNEXT)")
234 raise ValueError("cannot get UIDNEXT: %s", typ)
235 match = re.match("[^ ]* \(UIDNEXT ([0-9]+)\)", dat[0])
237 raise ValueError("cannot match UIDNEXT: %s", typ)
238 self.uidnext[m] = int(match.groups()[0])
239 self.logger.info('Done: %r', self.uidnext)
241 def process_rules(self):
245 self.logger.info('Processing rules...')
246 for rule in self.rules:
248 self.logger.info('Done.')
252 Close the connection to the IMAP server.
255 self.logger.info('Connection closed.')
257 def show_config_help():
259 Show the help related to the configuration file.
263 The configuration file is in YAML format. Refer to http://yaml.org or
264 http://pyyaml.org for details about this format.
266 Full examples can be found in configuration samples located in
267 imapami/config-samples.
269 Configuration parameters and variables
270 ======================================
273 doc = inspect.getdoc(Imapami)
274 helptxt += doc + '\n\n'
276 helptxt += 'Rules syntax\n'
277 helptxt += '============\n\n'
279 doc = inspect.getdoc(imapami.rules.ImapamiRule)
280 helptxt += doc + '\n\n'
282 conds = imapami.conditions.get()
283 conds_list = conds.keys()
285 helptxt += 'Conditions\n'
286 helptxt += '==========\n\n'
288 helptxt += "%s\n" % c
289 helptxt += "-" * len(c) + '\n\n'
290 doc = inspect.getdoc(conds[c])
292 helptxt += doc + '\n\n'
294 helptxt += 'No help\n\n'
296 actions = imapami.actions.get()
297 actions_list = actions.keys()
299 helptxt += 'Actions\n'
300 helptxt += '=======\n\n'
302 helptxt += "%s\n" % a
303 helptxt += "-" * len(a) + '\n\n'
304 doc = inspect.getdoc(actions[a])
306 helptxt += doc + '\n\n'
308 helptxt += 'No help\n\n'
313 Run imapami: parse arguments, and launch imapami.
315 parser = argparse.ArgumentParser(
316 description='Process emails stored on an IMAP server.')
319 help='path configuration file (mandatory)')
321 '-C', '--check', action='store_true', default=False,
322 help='Only parse configuration and exit')
324 '-H', '--config-help', dest='config_help', action='store_true',
325 default=False, help='Show help about configuration file')
328 help='Console debug level, from 0 (no output) to 4 (debug).',
331 args = parser.parse_args()
332 if args.config_help == True:
336 if args.config is None:
337 sys.stderr.write('No config file specified\n')
341 p = Imapami(open(args.config).read(), args.debug)
342 if args.check == True:
343 sys.stderr.write('Configuration parsing ok\n')
351 if __name__ == '__main__':