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
43 _LOGLEVELS = [logging.CRITICAL, logging.ERROR, logging.WARNING,
44 logging.INFO, logging.DEBUG]
46 class Imapami(object):
48 The yaml configuration is a collection (dictionary). The following keys
51 - server: <string> [mandatory]
52 Hostname or IP address of the IMAP server to connect to.
54 - port: <integer> [optional]
55 The TCP port to connect to. If not specified, use the default,
56 depending on whether SSL is enabled or disabled.
58 - ssl: <boolean> [optional, default is True]
59 Enable or disable SSL (True or False).
61 - login: <string> [optional]
62 IMAP login name. If not specified, the login is asked on stdin.
64 - password: <string> [optional]
65 IMAP password. If not specified, the password is asked on stdin.
67 - logfile: <string> [optional]
68 File where messages are logged. If not specified, no logs are
71 - loglevel: <integer> [optional]
72 Level of logs written in logfile. Possible value are from 0 (no log)
73 to 4 (debug). Default value is 3 (info).
75 - inbox: <string> [optional, default is INBOX]
76 Default mailbox directory where rules get message from. It can be
77 overriden inside the rule.
79 - rules: <list of rules> [mandatory]
80 The list of rules to be applied. See the rule syntax for details.
82 Any key entry that is not reserved for a configuration parameter is
83 saved as a variable. The variables can be used in rules by referencing
84 them as {my_var} in conditions or actions arguments.
86 Variables are text only, it is possible to test them in rule conditions
87 and set them in rule actions.
90 def __init__(self, config, loglevel=3):
92 Create a Imapami object.
95 The yaml configuration.
96 :arg integer loglevel:
97 The level of the console logs.
100 self.config = {"server": None,
111 self.logger = self._get_logger(loglevel)
112 self._load_config(config)
113 self._update_logger()
115 def _get_logger(self, loglevel):
117 Create a logger, and configure it to log on console. This is done
118 before configuration is parsed.
120 :arg integer loglevel:
121 The level of the console logs from 0 (critical) to 4 (debug).
124 if not isinstance(loglevel, int) or loglevel < 0 or loglevel > 4:
125 sys.stderr.write("Invalid log level\n")
128 # create a logger and a stream handler on console
129 logger = logging.getLogger('imapami')
130 logger.setLevel(logging.DEBUG)
131 console_handler = logging.StreamHandler()
132 console_handler.setLevel(_LOGLEVELS[loglevel])
133 formatter = logging.Formatter('%(levelname)s - %(message)s')
134 console_handler.setFormatter(formatter)
135 logger.addHandler(console_handler)
136 self.console_handler = console_handler
139 def _update_logger(self):
141 Update the logger used after the configuration is parsed: add
142 the specified log file in addition to console.
144 logfile = self.config.get("logfile")
145 # continue to log on console
148 # else, add the new file handler
149 file_handler = logging.FileHandler(logfile)
150 loglevel = self.config.get("loglevel")
151 file_handler.setLevel(_LOGLEVELS[loglevel])
152 formatter = logging.Formatter(
153 '%(asctime)s - %(levelname)s - %(message)s')
154 file_handler.setFormatter(formatter)
155 self.logger.addHandler(file_handler)
157 def _load_config(self, config):
159 Load the yaml configuration. It parses the config parameters, the
160 variables, and the list of rules.
163 The yaml configuration.
165 yaml_dict = yaml.safe_load(config)
166 for key in yaml_dict:
167 if key in self.config:
168 self.config[key] = yaml_dict[key]
169 self.logger.debug("set config %s = %s", key, yaml_dict[key])
171 self.variables[key] = yaml_dict[key]
172 self.logger.debug("set variable %s = %s", key, yaml_dict[key])
173 for key in ["server", "ssl", "rules"]:
174 if self.config[key] is None:
175 self.logger.error("%s is not specified in configuration")
177 rules = self.config["rules"]
179 self.logger.error("no rule defined")
181 for i, rule in enumerate(rules):
183 self.rules.append(imapami.rules.new(rule))
184 except Exception as e:
185 self.logger.error("error while processing rule %d: %s",
186 i+1, rule.get('name', 'no-name'))
191 Connect and login to the remote IMAP server.
193 if self.config.get("ssl") == True:
194 imap_class = imaplib.IMAP4_SSL
196 imap_class = imaplib.IMAP4
198 login = self.config.get("login")
200 login = raw_input("Username: ")
201 password = self.config.get("password")
203 password = getpass.getpass()
204 server = self.config.get("server")
205 port = self.config.get("port")
207 imap = imap_class(server)
209 imap = imap_class(server, port)
210 self.logger.info('Connecting to %s@%s...', login, server)
211 imap.login(login, password)
214 def process_rules(self):
218 self.logger.info('Processing rules...')
219 inbox = self.config["inbox"]
220 for rule in self.rules:
221 rule.process(self, inbox)
222 self.logger.info('Done.')
226 Close the connection to the IMAP server.
229 self.logger.info('Connection closed.')
231 def show_config_help():
233 Show the help related to the configuration file.
237 The configuration file is in YAML format. Refer to http://yaml.org or
238 http://pyyaml.org for details about this format.
240 Full examples can be found in configuration samples located in
241 imapami/config-samples.
243 Configuration parameters and variables
244 ======================================
247 doc = inspect.getdoc(Imapami)
248 helptxt += doc + '\n\n'
250 helptxt += 'Rules syntax\n'
251 helptxt += '============\n\n'
253 doc = inspect.getdoc(imapami.rules.ImapamiRule)
254 helptxt += doc + '\n\n'
256 conds = imapami.conditions.get()
257 conds_list = conds.keys()
259 helptxt += 'Conditions\n'
260 helptxt += '==========\n\n'
262 helptxt += "%s\n" % c
263 helptxt += "-" * len(c) + '\n\n'
264 doc = inspect.getdoc(conds[c])
266 helptxt += doc + '\n\n'
268 helptxt += 'No help\n\n'
270 actions = imapami.actions.get()
271 actions_list = actions.keys()
273 helptxt += 'Actions\n'
274 helptxt += '=======\n\n'
276 helptxt += "%s\n" % a
277 helptxt += "-" * len(a) + '\n\n'
278 doc = inspect.getdoc(actions[a])
280 helptxt += doc + '\n\n'
282 helptxt += 'No help\n\n'
287 Run imapami: parse arguments, and launch imapami.
289 parser = argparse.ArgumentParser(
290 description='Process emails stored on an IMAP server.')
293 help='path configuration file (mandatory)')
295 '-C', '--check', action='store_true', default=False,
296 help='Only parse configuration and exit')
298 '-H', '--config-help', dest='config_help', action='store_true',
299 default=False, help='Show help about configuration file')
302 help='Console debug level, from 0 (no output) to 4 (debug).',
305 args = parser.parse_args()
306 if args.config_help == True:
310 if args.config is None:
311 sys.stderr.write('No config file specified\n')
315 p = Imapami(open(args.config).read(), args.debug)
316 if args.check == True:
317 sys.stderr.write('Configuration parsing ok\n')
324 if __name__ == '__main__':