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 criteria = 'NOT (%s)' % (cond.get_criteria())
132 ImapamiCond.__init__(self, fetch=cond.fetch, criteria=criteria)
135 def check(self, ami, mail):
136 return not self.cond.check(ami, mail)
137 register(ImapamiCondNot)
139 class ImapamiCondUnseen(ImapamiCond):
141 Match if a mail is not marked as seen.
143 This condition does not require to fetch the mail as it can
144 be filtered by the IMAP server.
154 ImapamiCond.__init__(self, criteria=set(["UNSEEN"]))
155 register(ImapamiCondUnseen)
157 class ImapamiCondFrom(ImapamiCond):
159 Match if a 'From' field contains the specified substring.
161 This condition does not require to fetch the mail as it can
162 be filtered by the IMAP server.
166 The substring that should be included in the 'From' header
170 from: {substr: foo@example.com}
173 def __init__(self, substr):
174 ImapamiCond.__init__(self, criteria=set(['FROM "%s"' % substr]))
175 register(ImapamiCondFrom)
177 class ImapamiCondSubject(ImapamiCond):
179 Match if a 'Subject' field contains the specified substring.
181 This condition does not require to fetch the mail as it can
182 be filtered by the IMAP server.
186 The substring that should be included in the 'Subject' header
190 subject: {substr: foo@example.com}
193 def __init__(self, substr):
194 ImapamiCond.__init__(self, criteria=set(['SUBJECT "%s"' % substr]))
195 register(ImapamiCondSubject)
197 class ImapamiCondTo(ImapamiCond):
199 Match if a 'To' field contains the specified substring.
201 This condition does not require to fetch the mail as it can
202 be filtered by the IMAP server.
206 The substring that should be included in the 'To' header
210 to: {substr: foo@example.com}
213 def __init__(self, substr):
214 ImapamiCond.__init__(self, criteria=set(['TO "%s"' % substr]))
215 register(ImapamiCondTo)
217 class ImapamiCondCc(ImapamiCond):
219 Match if a 'Cc' field contains the specified substring.
221 This condition does not require to fetch the mail as it can
222 be filtered by the IMAP server.
226 The substring that should be included in the 'Cc' header
230 cc: {substr: foo@example.com}
233 def __init__(self, substr):
234 ImapamiCond.__init__(self, criteria=set(['CC "%s"' % substr]))
235 register(ImapamiCondCc)
237 class ImapamiCondBcc(ImapamiCond):
239 Match if a 'Bcc' field contains the specified substring.
241 This condition does not require to fetch the mail as it can
242 be filtered by the IMAP server.
246 The substring that should be included in the 'Bcc' header
250 bcc: {substr: foo@example.com}
253 def __init__(self, substr):
254 ImapamiCond.__init__(self, criteria=set(['BCC "%s"' % substr]))
255 register(ImapamiCondBcc)
257 class ImapamiCondBody(ImapamiCond):
259 Match if the body of the message contains the specified substring.
261 This condition does not require to fetch the mail as it can
262 be filtered by the IMAP server.
266 The substring that should be included in the 'Body' header
270 body: {substr: foobar}
273 def __init__(self, substr):
274 ImapamiCond.__init__(self, criteria=set(['BODY "%s"' % substr]))
275 register(ImapamiCondBody)
277 class ImapamiCondSince(ImapamiCond):
279 Match if the message has been sent since the specified date.
281 Match for messages whose internal date (disregarding time and timezone)
282 is within or later than the specified date.
284 This condition does not require to fetch the mail as it can
285 be filtered by the IMAP server.
289 The reference date. The format is day-month-year, with:
290 day = 2 digits, month = 3 letter string (Jan, Feb, Mar, Apr,
291 May, Jun, Jul, Aug, Sep, Oct, Nov, Dec), year = 4 digits.
294 since: {date: 28-Oct-2015}
297 def __init__(self, date):
298 ImapamiCond.__init__(self, criteria=set(['SINCE "%s"' % date]))
299 register(ImapamiCondSince)
301 class ImapamiCondSentBefore(ImapamiCond):
303 Match if 'Date' header is earlier than the specified date (disregarding
306 This condition does not require to fetch the mail as it can
307 be filtered by the IMAP server.
310 sent-before: <string>
311 The reference date. The format is day-month-year, with:
312 day = 2 digits, month = 3 letter string (Jan, Feb, Mar, Apr,
313 May, Jun, Jul, Aug, Sep, Oct, Nov, Dec), year = 4 digits.
316 sent-before: {date: 28-Oct-2015}
319 def __init__(self, date):
320 ImapamiCond.__init__(self, criteria=set(['SENTBEFORE "%s"' % date]))
321 register(ImapamiCondSentBefore)
323 class ImapamiCondSentOn(ImapamiCond):
325 Match if 'Date' header is within the specified date (disregarding
328 This condition does not require to fetch the mail as it can
329 be filtered by the IMAP server.
333 The reference date. The format is day-month-year, with:
334 day = 2 digits, month = 3 letter string (Jan, Feb, Mar, Apr,
335 May, Jun, Jul, Aug, Sep, Oct, Nov, Dec), year = 4 digits.
338 sent-on: {date: 28-Oct-2015}
341 def __init__(self, date):
342 ImapamiCond.__init__(self, criteria=set(['SENTON "%s"' % date]))
343 register(ImapamiCondSentOn)
345 class ImapamiCondSentSince(ImapamiCond):
347 Match if 'Date' header is within or later than the specified date
348 (disregarding time and timezone).
350 This condition does not require to fetch the mail as it can
351 be filtered by the IMAP server.
355 The reference date. The format is day-month-year, with:
356 day = 2 digits, month = 3 letter string (Jan, Feb, Mar, Apr,
357 May, Jun, Jul, Aug, Sep, Oct, Nov, Dec), year = 4 digits.
360 sent-since: {date: 28-Oct-2015}
363 def __init__(self, date):
364 ImapamiCond.__init__(self, criteria=set(['SENTSINCE "%s"' % date]))
365 register(ImapamiCondSentSince)
367 class ImapamiCondRecent(ImapamiCond):
369 Match if the mail is marked as recent
371 This condition does not require to fetch the mail as it can
372 be filtered by the IMAP server.
381 def __init__(self, substr):
382 ImapamiCond.__init__(self, criteria=set(['RECENT']))
383 register(ImapamiCondRecent)
385 class ImapamiCondAnswered(ImapamiCond):
387 Match if the mail is not marked as answered.
389 This condition does not require to fetch the mail as it can
390 be filtered by the IMAP server.
399 def __init__(self, substr):
400 ImapamiCond.__init__(self, criteria=set(['ANSWERED']))
401 register(ImapamiCondAnswered)
403 class ImapamiCondUnanswered(ImapamiCond):
405 Match if the mail is not marked as answered.
407 This condition does not require to fetch the mail as it can
408 be filtered by the IMAP server.
417 def __init__(self, substr):
418 ImapamiCond.__init__(self, criteria=set(['UNANSWERED']))
419 register(ImapamiCondUnanswered)
421 class ImapamiCondFlagged(ImapamiCond):
423 Match if the message is flagged.
425 This condition does not require to fetch the mail as it can
426 be filtered by the IMAP server.
435 def __init__(self, substr):
436 ImapamiCond.__init__(self, criteria=set(['FLAGGED']))
437 register(ImapamiCondFlagged)
439 class ImapamiCondUnflagged(ImapamiCond):
441 Match if the message is not flagged.
443 This condition does not require to fetch the mail as it can
444 be filtered by the IMAP server.
453 def __init__(self, substr):
454 ImapamiCond.__init__(self, criteria=set(['UNFLAGGED']))
455 register(ImapamiCondUnflagged)
457 class ImapamiCondDraft(ImapamiCond):
459 Match if the message is marked as draft.
461 This condition does not require to fetch the mail as it can
462 be filtered by the IMAP server.
471 def __init__(self, substr):
472 ImapamiCond.__init__(self, criteria=set(['DRAFT']))
473 register(ImapamiCondDraft)
475 class ImapamiCondUndraft(ImapamiCond):
477 Match if the message is not marked as draft.
479 This condition does not require to fetch the mail as it can
480 be filtered by the IMAP server.
489 def __init__(self, substr):
490 ImapamiCond.__init__(self, criteria=set(['UNDRAFT']))
491 register(ImapamiCondUndraft)
493 class ImapamiCondKeyword(ImapamiCond):
495 Match if the messages has the specified keyword flag set.
497 This condition does not require to fetch the mail as it can
498 be filtered by the IMAP server.
505 keyword: {key: important}
508 def __init__(self, key):
509 ImapamiCond.__init__(self, criteria=set(['KEYWORD "%s"' % key]))
510 register(ImapamiCondKeyword)
512 class ImapamiCondUnkeyword(ImapamiCond):
514 Match if the messages does not have the specified keyword flag set.
516 This condition does not require to fetch the mail as it can
517 be filtered by the IMAP server.
524 unkeyword: {key: important}
527 def __init__(self, key):
528 ImapamiCond.__init__(self, criteria=set(['UNKEYWORD "%s"' % key]))
529 register(ImapamiCondUnkeyword)
531 class ImapamiCondLarger(ImapamiCond):
533 Match if the message is larger than the specified size.
535 This condition does not require to fetch the mail as it can
536 be filtered by the IMAP server.
540 The size of the message in bytes.
546 def __init__(self, size):
547 ImapamiCond.__init__(self, criteria=set(['LARGER "%s"' % size]))
548 register(ImapamiCondLarger)
550 class ImapamiCondSmaller(ImapamiCond):
552 Match if the message is smaller than the specified size.
554 This condition does not require to fetch the mail as it can
555 be filtered by the IMAP server.
559 The size of the message in bytes.
562 smaller: {size: 1024}
565 def __init__(self, size):
566 ImapamiCond.__init__(self, criteria=set(['SMALLER "%s"' % size]))
567 register(ImapamiCondSmaller)
569 class ImapamiCondRegex(ImapamiCond):
571 Match if the regular expression is found in the specified field.
573 This condition requires at least to fetch the mail headers, or the
574 full message if the regexp is researched in the body of the mail.
578 The name of the header field (ex: 'From', 'To', ...) where the
579 regular expression is researched or 'part1'/'all' if the regular
580 expression is researched in the body of the mail ('all' includes
583 The regular expression, as supported by the python re module.
586 regexp: {field: Subject, pattern: '\\[my_list\\]'}
589 def __init__(self, field, pattern):
596 ImapamiCond.__init__(self, fetch=fetch)
598 self.pattern = pattern
599 def check(self, ami, mail):
600 field = self.evaluate(self.field, ami, mail.msg)
601 pattern = self.evaluate(self.pattern, ami,
604 m = re.search(pattern, mail.body_part1)
606 m = re.search(pattern, mail.body_all)
608 data = mail.msg.get(field)
611 m = re.search(pattern, data)
616 register(ImapamiCondRegex)
618 class ImapamiCondAnd(ImapamiCond):
620 Match if all conditions of a list match (AND).
622 This command is used internally when the list of conditions of a
623 rule is parsed. It can also be used by a user to group conditions:
624 as soon as a condition does not match, this meta condition returns
625 False (lazy evaluation).
632 - regexp: {field: From, pattern: foo}
633 - regexp: {field: Subject, pattern: bar}
636 def __init__(self, *cond_list):
637 cond_list = [new(c) for c in cond_list]
638 criteria = set().union(*[c.criteria for c in cond_list])
639 fetch = imapami.utils.highest_fetch_level(
640 [c.fetch for c in cond_list])
641 ImapamiCond.__init__(self, fetch=fetch, criteria=criteria)
642 self.cond_list = cond_list
644 def check(self, ami, mail):
645 for c in self.cond_list:
646 if c.check(ami, mail) == False:
649 register(ImapamiCondAnd)
651 class ImapamiCondOr(ImapamiCond):
653 Match if at least one condition of a list matches (OR).
655 Try to match the conditions of the list: as soon as a condition matches,
656 this meta condition returns True (lazy evaluation).
663 - regexp: {field: From, pattern: foo}
664 - regexp: {field: Subject, pattern: bar}
667 def __init__(self, *cond_list):
668 cond_list = [new(c) for c in cond_list]
671 crit = c.get_criteria()
677 criteria = 'OR (%s) (%s)' % (criteria, crit)
679 criteria = set([criteria])
680 fetch = imapami.utils.highest_fetch_level(
681 [c.fetch for c in cond_list])
682 ImapamiCond.__init__(self, fetch=fetch, criteria=criteria)
683 self.cond_list = cond_list
685 def check(self, ami, mail):
686 for c in self.cond_list:
687 if c.check(ami, mail) == True:
690 register(ImapamiCondOr)
692 class ImapamiCondEq(ImapamiCond):
694 Match if strings are equal.
696 This condition does not fetch any part of the mail by default.
697 If a mail header field is used as a variable in the condition
698 parameters, the user must ensure that the mail headers are fetched,
699 for instance by setting the rule paramater 'fetch' to 'headers'.
701 If more than 2 elements are given in the list, all of them must
708 eq: ['foo@example.com', '{From}', '{foo}']
711 def __init__(self, *str_list):
712 ImapamiCond.__init__(self)
713 if not isinstance(str_list, list) and not isinstance(str_list, tuple):
714 raise ValueError("arguments of 'eq' should be a list/tuple")
715 if len(str_list) < 2:
716 raise ValueError("'eq' argument list is too short")
717 self.str_list = list(str_list)
719 def check(self, ami, mail):
720 first = self.evaluate(self.str_list[0], ami,
722 for s in self.str_list[1:]:
723 if self.evaluate(s, ami, mail.msg) != first:
726 register(ImapamiCondEq)
730 Create a condition object from its yaml config.
733 The yaml condition config.
735 The condition object.
737 logger = logging.getLogger('imapami')
738 logger.debug("parsing condition %s", config)
740 raise ValueError("the condition config must be a dictionary whose only "
741 "key is the condition name")
742 cond_name = config.keys()[0]
745 if isinstance(config[cond_name], list):
746 argslist = config[cond_name]
747 elif isinstance(config[cond_name], dict):
748 argsdict = config[cond_name]
749 elif config[cond_name] is not None:
750 argslist = [config[cond_name]]
751 cond_class = _all_conditions.get(cond_name)
752 if cond_class is None:
753 raise ValueError("Invalid condition name '%s'" % cond_name)
754 logger.debug("new cond %s(%s, %s)", cond_name, argslist, argsdict)
755 return cond_class(*argslist, **argsdict)
759 Return a dictionary containing all the registered conditions.
761 return _all_conditions