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.
37 def register(cond_class):
39 Register a condition class
41 if not issubclass(cond_class, ImapamiCond):
42 raise ValueError('"%s" condition is not a subclass of ImapamiCond' %
44 if cond_class.name in _all_conditions:
45 raise ValueError('"%s" condition is already registered' %
47 _all_conditions[cond_class.name] = cond_class
49 class ImapamiCond(object):
51 This is the parent class for conditions.
53 A condition is a test performed on the mails located in a
54 specific mailbox directory. For instance, a condition can check
55 the sender, the subject, the date of a mail, or the content of
58 def __init__(self, fetch=None, criteria=None):
60 Generic condition constructor.
63 Set to "headers", "part1", "all" if it is needed to fetch
64 the header, the body or the attachments of the mail
66 IMAP criteria for IMAP search command. Ex: set(["UNSEEN"])
68 self.fetch = fetch or "no"
69 self.criteria = criteria or set()
71 def evaluate(self, arg, ami, hdrs):
73 Evaluate a string argument
75 Replace variables and headers in arg by their values and
79 The argument to be evaluated
81 The imapami object, containing the variables
82 :arg email.message.Message hdrs:
83 The headers of the mail, or None if not available
85 The evaluated argument.
88 variables = imapami.utils.headers_to_unicode(hdrs)
91 variables.update(ami.variables)
92 fmt = imapami.utils.VarFormatter()
93 arg = fmt.format(unicode(arg), **variables)
96 def check(self, ami, mail):
102 :arg ImapamiMail mail:
105 True if the condition matches, else False.
109 def get_criteria(self):
111 Get the list of IMAP criteria that are passed to the server inside
112 the IMAP search command.
113 The caller will evaluate the variables, so it's not to be done in the
116 return " ".join([str(c) for c in self.criteria])
118 class ImapamiCondNot(ImapamiCond):
120 Invert the result of a condition.
126 not: {from: foo@example.com}
129 def __init__(self, cond):
131 ImapamiCond.__init__(self, fetch=cond.fetch, criteria=cond.criteria)
134 def check(self, ami, mail):
135 return not self.cond.check(ami, mail)
136 register(ImapamiCondNot)
138 class ImapamiCondUnseen(ImapamiCond):
140 Match if a mail is not marked as seen.
142 This condition does not require to fetch the mail as it can
143 be filtered by the IMAP server.
153 ImapamiCond.__init__(self, criteria=set(["UNSEEN"]))
154 register(ImapamiCondUnseen)
156 class ImapamiCondFrom(ImapamiCond):
158 Match if a 'From' field contains the specified substring.
160 This condition does not require to fetch the mail as it can
161 be filtered by the IMAP server.
165 The substring that should be included in the 'From' header
169 from: {substr: foo@example.com}
172 def __init__(self, substr):
173 ImapamiCond.__init__(self, criteria=set(['FROM "%s"' % substr]))
174 register(ImapamiCondFrom)
176 class ImapamiCondSubject(ImapamiCond):
178 Match if a 'Subject' field contains the specified substring.
180 This condition does not require to fetch the mail as it can
181 be filtered by the IMAP server.
185 The substring that should be included in the 'Subject' header
189 subject: {substr: foo@example.com}
192 def __init__(self, substr):
193 ImapamiCond.__init__(self, criteria=set(['SUBJECT "%s"' % substr]))
194 register(ImapamiCondSubject)
196 class ImapamiCondTo(ImapamiCond):
198 Match if a 'To' field contains the specified substring.
200 This condition does not require to fetch the mail as it can
201 be filtered by the IMAP server.
205 The substring that should be included in the 'To' header
209 to: {substr: foo@example.com}
212 def __init__(self, substr):
213 ImapamiCond.__init__(self, criteria=set(['TO "%s"' % substr]))
214 register(ImapamiCondTo)
216 class ImapamiCondCc(ImapamiCond):
218 Match if a 'Cc' field contains the specified substring.
220 This condition does not require to fetch the mail as it can
221 be filtered by the IMAP server.
225 The substring that should be included in the 'Cc' header
229 cc: {substr: foo@example.com}
232 def __init__(self, substr):
233 ImapamiCond.__init__(self, criteria=set(['CC "%s"' % substr]))
234 register(ImapamiCondCc)
236 class ImapamiCondBcc(ImapamiCond):
238 Match if a 'Bcc' field contains the specified substring.
240 This condition does not require to fetch the mail as it can
241 be filtered by the IMAP server.
245 The substring that should be included in the 'Bcc' header
249 bcc: {substr: foo@example.com}
252 def __init__(self, substr):
253 ImapamiCond.__init__(self, criteria=set(['BCC "%s"' % substr]))
254 register(ImapamiCondBcc)
256 class ImapamiCondBody(ImapamiCond):
258 Match if the body of the message contains the specified substring.
260 This condition does not require to fetch the mail as it can
261 be filtered by the IMAP server.
265 The substring that should be included in the 'Body' header
269 body: {substr: foobar}
272 def __init__(self, substr):
273 ImapamiCond.__init__(self, criteria=set(['BODY "%s"' % substr]))
274 register(ImapamiCondBody)
276 class ImapamiCondSince(ImapamiCond):
278 Match if the message has been sent since the specified date.
280 Match for messages whose internal date (disregarding time and timezone)
281 is within or later than the specified date.
283 This condition does not require to fetch the mail as it can
284 be filtered by the IMAP server.
288 The reference date. The format is day-month-year, with:
289 day = 2 digits, month = 3 letter string (Jan, Feb, Mar, Apr,
290 May, Jun, Jul, Aug, Sep, Oct, Nov, Dec), year = 4 digits.
293 since: {date: 28-Oct-2015}
296 def __init__(self, date):
297 ImapamiCond.__init__(self, criteria=set(['SINCE "%s"' % date]))
298 register(ImapamiCondSince)
300 class ImapamiCondSentBefore(ImapamiCond):
302 Match if 'Date' header is earlier than the specified date (disregarding
305 This condition does not require to fetch the mail as it can
306 be filtered by the IMAP server.
309 sent-before: <string>
310 The reference date. The format is day-month-year, with:
311 day = 2 digits, month = 3 letter string (Jan, Feb, Mar, Apr,
312 May, Jun, Jul, Aug, Sep, Oct, Nov, Dec), year = 4 digits.
315 sent-before: {date: 28-Oct-2015}
318 def __init__(self, date):
319 ImapamiCond.__init__(self, criteria=set(['SENTBEFORE "%s"' % date]))
320 register(ImapamiCondSentBefore)
322 class ImapamiCondSentOn(ImapamiCond):
324 Match if 'Date' header is within the specified date (disregarding
327 This condition does not require to fetch the mail as it can
328 be filtered by the IMAP server.
332 The reference date. The format is day-month-year, with:
333 day = 2 digits, month = 3 letter string (Jan, Feb, Mar, Apr,
334 May, Jun, Jul, Aug, Sep, Oct, Nov, Dec), year = 4 digits.
337 sent-on: {date: 28-Oct-2015}
340 def __init__(self, date):
341 ImapamiCond.__init__(self, criteria=set(['SENTON "%s"' % date]))
342 register(ImapamiCondSentOn)
344 class ImapamiCondSentSince(ImapamiCond):
346 Match if 'Date' header is within or later than the specified date
347 (disregarding time and timezone).
349 This condition does not require to fetch the mail as it can
350 be filtered by the IMAP server.
354 The reference date. The format is day-month-year, with:
355 day = 2 digits, month = 3 letter string (Jan, Feb, Mar, Apr,
356 May, Jun, Jul, Aug, Sep, Oct, Nov, Dec), year = 4 digits.
359 sent-since: {date: 28-Oct-2015}
362 def __init__(self, date):
363 ImapamiCond.__init__(self, criteria=set(['SENTSINCE "%s"' % date]))
364 register(ImapamiCondSentSince)
366 class ImapamiCondRecent(ImapamiCond):
368 Match if the mail is marked as recent
370 This condition does not require to fetch the mail as it can
371 be filtered by the IMAP server.
380 def __init__(self, substr):
381 ImapamiCond.__init__(self, criteria=set(['RECENT']))
382 register(ImapamiCondRecent)
384 class ImapamiCondAnswered(ImapamiCond):
386 Match if the mail is not marked as answered.
388 This condition does not require to fetch the mail as it can
389 be filtered by the IMAP server.
398 def __init__(self, substr):
399 ImapamiCond.__init__(self, criteria=set(['ANSWERED']))
400 register(ImapamiCondAnswered)
402 class ImapamiCondUnanswered(ImapamiCond):
404 Match if the mail is not marked as answered.
406 This condition does not require to fetch the mail as it can
407 be filtered by the IMAP server.
416 def __init__(self, substr):
417 ImapamiCond.__init__(self, criteria=set(['UNANSWERED']))
418 register(ImapamiCondUnanswered)
420 class ImapamiCondFlagged(ImapamiCond):
422 Match if the message is flagged.
424 This condition does not require to fetch the mail as it can
425 be filtered by the IMAP server.
434 def __init__(self, substr):
435 ImapamiCond.__init__(self, criteria=set(['FLAGGED']))
436 register(ImapamiCondFlagged)
438 class ImapamiCondUnflagged(ImapamiCond):
440 Match if the message is not flagged.
442 This condition does not require to fetch the mail as it can
443 be filtered by the IMAP server.
452 def __init__(self, substr):
453 ImapamiCond.__init__(self, criteria=set(['UNFLAGGED']))
454 register(ImapamiCondUnflagged)
456 class ImapamiCondDraft(ImapamiCond):
458 Match if the message is marked as draft.
460 This condition does not require to fetch the mail as it can
461 be filtered by the IMAP server.
470 def __init__(self, substr):
471 ImapamiCond.__init__(self, criteria=set(['DRAFT']))
472 register(ImapamiCondDraft)
474 class ImapamiCondUndraft(ImapamiCond):
476 Match if the message is not marked as draft.
478 This condition does not require to fetch the mail as it can
479 be filtered by the IMAP server.
488 def __init__(self, substr):
489 ImapamiCond.__init__(self, criteria=set(['UNDRAFT']))
490 register(ImapamiCondUndraft)
492 class ImapamiCondKeyword(ImapamiCond):
494 Match if the messages has the specified keyword flag set.
496 This condition does not require to fetch the mail as it can
497 be filtered by the IMAP server.
504 keyword: {key: important}
507 def __init__(self, key):
508 ImapamiCond.__init__(self, criteria=set(['KEYWORD "%s"' % key]))
509 register(ImapamiCondKeyword)
511 class ImapamiCondUnkeyword(ImapamiCond):
513 Match if the messages does not have the specified keyword flag set.
515 This condition does not require to fetch the mail as it can
516 be filtered by the IMAP server.
523 unkeyword: {key: important}
526 def __init__(self, key):
527 ImapamiCond.__init__(self, criteria=set(['UNKEYWORD "%s"' % key]))
528 register(ImapamiCondUnkeyword)
530 class ImapamiCondLarger(ImapamiCond):
532 Match if the message is larger than the specified size.
534 This condition does not require to fetch the mail as it can
535 be filtered by the IMAP server.
539 The size of the message in bytes.
545 def __init__(self, size):
546 ImapamiCond.__init__(self, criteria=set(['LARGER "%s"' % size]))
547 register(ImapamiCondLarger)
549 class ImapamiCondSmaller(ImapamiCond):
551 Match if the message is smaller than the specified size.
553 This condition does not require to fetch the mail as it can
554 be filtered by the IMAP server.
558 The size of the message in bytes.
561 smaller: {size: 1024}
564 def __init__(self, size):
565 ImapamiCond.__init__(self, criteria=set(['SMALLER "%s"' % size]))
566 register(ImapamiCondSmaller)
568 class ImapamiCondRegex(ImapamiCond):
570 Match if the regular expression is found in the specified field.
572 This condition requires at least to fetch the mail headers, or the
573 full message if the regexp is researched in the body of the mail.
577 The name of the header field (ex: 'From', 'To', ...) where the
578 regular expression is researched or 'part1'/'all' if the regular
579 expression is researched in the body of the mail ('all' includes
582 The regular expression, as supported by the python re module.
585 regexp: {field: Subject, pattern: '\\[my_list\\]'}
588 def __init__(self, field, pattern):
595 ImapamiCond.__init__(self, fetch=fetch)
597 self.pattern = pattern
598 def check(self, ami, mail):
599 field = self.evaluate(self.field, ami, mail.msg)
600 pattern = self.evaluate(self.pattern, ami,
603 m = re.search(pattern, mail.body_part1)
605 m = re.search(pattern, mail.body_all)
607 data = mail.msg.get(field)
610 m = re.search(pattern, data)
615 register(ImapamiCondRegex)
617 class ImapamiCondAnd(ImapamiCond):
619 Match if all conditions of a list match (AND).
621 This command is used internally when the list of conditions of a
622 rule is parsed. It can also be used by a user to group conditions:
623 as soon as a condition does not match, this meta condition returns
624 False (lazy evaluation).
631 - regexp: {field: From, pattern: foo}
632 - regexp: {field: Subject, pattern: bar}
635 def __init__(self, *cond_list):
636 cond_list = [new(c) for c in cond_list]
637 criteria = set().union(*[c.criteria for c in cond_list])
638 fetch = imapami.utils.highest_fetch_level(
639 [c.fetch for c in cond_list])
640 ImapamiCond.__init__(self, fetch=fetch, criteria=criteria)
641 self.cond_list = cond_list
643 def check(self, ami, mail):
644 for c in self.cond_list:
645 if c.check(ami, mail) == False:
648 register(ImapamiCondAnd)
650 class ImapamiCondOr(ImapamiCond):
652 Match if at least one condition of a list matches (OR).
654 Try to match the conditions of the list: as soon as a condition matches,
655 this meta condition returns True (lazy evaluation).
662 - regexp: {field: From, pattern: foo}
663 - regexp: {field: Subject, pattern: bar}
666 def __init__(self, *cond_list):
667 cond_list = [new(c) for c in cond_list]
670 crit = c.get_criteria()
676 criteria = 'OR (%s) (%s)' % (criteria, crit)
678 criteria = set().add(criteria)
679 fetch = imapami.utils.highest_fetch_level(
680 [c.fetch for c in cond_list])
681 ImapamiCond.__init__(self, fetch=fetch, criteria=criteria)
682 self.cond_list = cond_list
684 def check(self, ami, mail):
685 for c in self.cond_list:
686 if c.check(ami, mail) == True:
689 register(ImapamiCondOr)
691 class ImapamiCondEq(ImapamiCond):
693 Match if strings are equal.
695 This condition does not fetch any part of the mail by default.
696 If a mail header field is used as a variable in the condition
697 parameters, the user must ensure that the mail headers are fetched,
698 for instance by setting the rule paramater 'fetch' to 'headers'.
700 If more than 2 elements are given in the list, all of them must
707 eq: ['foo@example.com', '{From}', '{foo}']
710 def __init__(self, *str_list):
711 ImapamiCond.__init__(self)
712 if not isinstance(str_list, list) and not isinstance(str_list, tuple):
713 raise ValueError("arguments of 'eq' should be a list/tuple")
714 if len(str_list) < 2:
715 raise ValueError("'eq' argument list is too short")
716 self.str_list = list(str_list)
718 def check(self, ami, mail):
719 first = self.evaluate(self.str_list[0], ami,
721 for s in self.str_list[1:]:
722 if self.evaluate(s, ami, mail.msg) != first:
725 register(ImapamiCondEq)
729 Create a condition object from its yaml config.
732 The yaml condition config.
734 The condition object.
736 logger = logging.getLogger('imapami')
737 logger.debug("parsing condition %s", config)
739 raise ValueError("the condition config must be a dictionary whose only "
740 "key is the condition name")
741 cond_name = config.keys()[0]
744 if isinstance(config[cond_name], list):
745 argslist = config[cond_name]
746 elif isinstance(config[cond_name], dict):
747 argsdict = config[cond_name]
748 elif config[cond_name] is not None:
749 argslist = [config[cond_name]]
750 cond_class = _all_conditions.get(cond_name)
751 if cond_class is None:
752 raise ValueError("Invalid condition name '%s'" % cond_name)
753 logger.debug("new cond %s(%s, %s)", cond_name, argslist, argsdict)
754 return cond_class(*argslist, **argsdict)
758 Return a dictionary containing all the registered conditions.
760 return _all_conditions