From 5d4ed5ed3f055f3dabf2388d8ccd30da81808db5 Mon Sep 17 00:00:00 2001 From: Lefteris Chatzimparmpas Date: Sun, 6 Mar 2011 12:58:58 +0100 Subject: [PATCH] Initial commit. --- AUTHORS | 1 + LICENSE | 19 + Makefile | 105 ++++ NEWS | 192 +++++++ README | 60 +++ account.lua | 239 +++++++++ auth.c | 73 +++ auxiliary.lua | 66 +++ buffer.c | 60 +++ buffer.h | 23 + cert.c | 235 ++++++++ common.lua | 510 ++++++++++++++++++ configure | 251 +++++++++ core.c | 1169 ++++++++++++++++++++++++++++++++++++++++ deprecated.lua | 609 +++++++++++++++++++++ file.c | 132 +++++ imap.c | 484 +++++++++++++++++ imapfilter.1 | 74 +++ imapfilter.c | 186 +++++++ imapfilter.h | 285 ++++++++++ imapfilter_config.5 | 1112 ++++++++++++++++++++++++++++++++++++++ list.c | 59 ++ list.h | 16 + log.c | 240 +++++++++ lua.c | 343 ++++++++++++ mailbox.lua | 1243 +++++++++++++++++++++++++++++++++++++++++++ memory.c | 92 ++++ message.lua | 93 ++++ misc.c | 74 +++ namespace.c | 78 +++ options.lua | 5 + pathnames.h | 45 ++ pcre.c | 268 ++++++++++ regex.lua | 53 ++ regexp.c | 42 ++ regexp.h | 24 + request.c | 918 ++++++++++++++++++++++++++++++++ response.c | 846 +++++++++++++++++++++++++++++ sample.config.lua | 129 +++++ sample.extend.lua | 103 ++++ session.c | 101 ++++ session.h | 37 ++ set.lua | 700 ++++++++++++++++++++++++ signal.c | 45 ++ socket.c | 440 +++++++++++++++ system.c | 308 +++++++++++ version.h | 12 + 47 files changed, 12199 insertions(+) create mode 100644 AUTHORS create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 NEWS create mode 100644 README create mode 100644 account.lua create mode 100644 auth.c create mode 100644 auxiliary.lua create mode 100644 buffer.c create mode 100644 buffer.h create mode 100644 cert.c create mode 100644 common.lua create mode 100755 configure create mode 100644 core.c create mode 100644 deprecated.lua create mode 100644 file.c create mode 100644 imap.c create mode 100644 imapfilter.1 create mode 100644 imapfilter.c create mode 100644 imapfilter.h create mode 100644 imapfilter_config.5 create mode 100644 list.c create mode 100644 list.h create mode 100644 log.c create mode 100644 lua.c create mode 100644 mailbox.lua create mode 100644 memory.c create mode 100644 message.lua create mode 100644 misc.c create mode 100644 namespace.c create mode 100644 options.lua create mode 100644 pathnames.h create mode 100644 pcre.c create mode 100644 regex.lua create mode 100644 regexp.c create mode 100644 regexp.h create mode 100644 request.c create mode 100644 response.c create mode 100644 sample.config.lua create mode 100644 sample.extend.lua create mode 100644 session.c create mode 100644 session.h create mode 100644 set.lua create mode 100644 signal.c create mode 100644 socket.c create mode 100644 system.c create mode 100644 version.h diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..a4f2506 --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +Lefteris Chatzimparmpas diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..94e5d8b --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2001-2011 Eleftherios Chatzimparmpas + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fe8231b --- /dev/null +++ b/Makefile @@ -0,0 +1,105 @@ +DESTDIR = +BINDIR = /usr/local/bin +SHAREDIR = /usr/local/share/imapfilter +MANDIR = /usr/local/man + +INCDIRS = -I/usr/local/include +LIBDIRS = -L/usr/local/lib + +MYCFLAGS = -Wall -O +MYLDFLAGS = + +DEFS = -DMAKEFILE_SHAREDIR='"$(SHAREDIR)"' + +CFLAGS = $(MYCFLAGS) $(DEFS) $(INCDIRS) +LDFLAGS = $(MYLDFLAGS) $(LIBDIRS) + +LIBS = -lm -llua -lpcre -lssl -lcrypto + +MAN_BIN = imapfilter.1 +MAN_CONFIG = imapfilter_config.5 + +COMMON_LUA = common.lua +SET_LUA = set.lua +REGEX_LUA = regex.lua +ACCOUNT_LUA = account.lua +MAILBOX_LUA = mailbox.lua +MESSAGE_LUA = message.lua +OPTIONS_LUA = options.lua +AUXILIARY_LUA = auxiliary.lua +DEPRECATED_LUA = deprecated.lua + +BIN = imapfilter +OBJ = auth.o buffer.o cert.o core.o file.o imap.o imapfilter.o list.o log.o \ + lua.o memory.o misc.o namespace.o pcre.o regexp.o request.o \ + response.o session.o signal.o socket.o system.o + +all: $(BIN) + +$(BIN): $(OBJ) + $(CC) -o $(BIN) $(LDFLAGS) $(OBJ) $(LIBS) + +$(OBJ): imapfilter.h +buffer.o imap.o imapfilter.o namespace.o request.o response.o: buffer.h +cert.o file.o imapfilter.o log.o lua.o: pathnames.h +imapfilter.o log.o session.o: list.h +imapfilter.o regexp.o response.o: regexp.h +auth.o cert.o imap.o imapfilter.o log.o request.o response.o session.o \ + socket.o: session.h +imapfilter.o: version.h + +install: $(BIN) + if test ! -d $(DESTDIR)$(BINDIR); then \ + mkdir -p $(DESTDIR)$(BINDIR); fi + cp -f $(BIN) $(DESTDIR)$(BINDIR) && \ + chmod 0755 $(DESTDIR)$(BINDIR)/$(BIN) + if test ! -d $(DESTDIR)$(SHAREDIR); then \ + mkdir -p $(DESTDIR)$(SHAREDIR); fi + cp -f $(COMMON_LUA) $(DESTDIR)$(SHAREDIR) && \ + chmod 0644 $(DESTDIR)$(SHAREDIR)/$(COMMON_LUA) + cp -f $(SET_LUA) $(DESTDIR)$(SHAREDIR) && \ + chmod 0644 $(DESTDIR)$(SHAREDIR)/$(SET_LUA) + cp -f $(REGEX_LUA) $(DESTDIR)$(SHAREDIR) && \ + chmod 0644 $(DESTDIR)$(SHAREDIR)/$(REGEX_LUA) + cp -f $(ACCOUNT_LUA) $(DESTDIR)$(SHAREDIR) && \ + chmod 0644 $(DESTDIR)$(SHAREDIR)/$(ACCOUNT_LUA) + cp -f $(MAILBOX_LUA) $(DESTDIR)$(SHAREDIR) && \ + chmod 0644 $(DESTDIR)$(SHAREDIR)/$(MAILBOX_LUA) + cp -f $(MESSAGE_LUA) $(DESTDIR)$(SHAREDIR) && \ + chmod 0644 $(DESTDIR)$(SHAREDIR)/$(MESSAGE_LUA) + cp -f $(OPTIONS_LUA) $(DESTDIR)$(SHAREDIR) && \ + chmod 0644 $(DESTDIR)$(SHAREDIR)/$(OPTIONS_LUA) + cp -f $(AUXILIARY_LUA) $(DESTDIR)$(SHAREDIR) && \ + chmod 0644 $(DESTDIR)$(SHAREDIR)/$(AUXILIARY_LUA) + cp -f $(DEPRECATED_LUA) $(DESTDIR)$(SHAREDIR) && \ + chmod 0644 $(DESTDIR)$(SHAREDIR)/$(DEPRECATED_LUA) + if test ! -d $(DESTDIR)$(MANDIR)/man1; then \ + mkdir -p $(DESTDIR)$(MANDIR)/man1; fi + cp -f $(MAN_BIN) $(DESTDIR)$(MANDIR)/man1 && \ + chmod 0644 $(DESTDIR)$(MANDIR)/man1/$(MAN_BIN) + if test ! -d $(DESTDIR)$(MANDIR)/man5; then \ + mkdir -p $(DESTDIR)$(MANDIR)/man5; fi + cp -f $(MAN_CONFIG) $(DESTDIR)$(MANDIR)/man5 && \ + chmod 0644 $(DESTDIR)$(MANDIR)/man5/$(MAN_CONFIG) + +deinstall: + rm -f $(DESTDIR)$(BINDIR)/$(BIN) \ + $(DESTDIR)$(SHAREDIR)/$(COMMON_LUA) \ + $(DESTDIR)$(SHAREDIR)/$(SET_LUA) \ + $(DESTDIR)$(SHAREDIR)/$(REGEX_LUA) \ + $(DESTDIR)$(SHAREDIR)/$(ACCOUNT_LUA) \ + $(DESTDIR)$(SHAREDIR)/$(MAILBOX_LUA) \ + $(DESTDIR)$(SHAREDIR)/$(MESSAGE_LUA) \ + $(DESTDIR)$(SHAREDIR)/$(OPTIONS_LUA) \ + $(DESTDIR)$(SHAREDIR)/$(AUXILIARY_LUA) \ + $(DESTDIR)$(SHAREDIR)/$(DEPRECATED_LUA) \ + $(DESTDIR)$(MANDIR)/man1/$(MAN_BIN) \ + $(DESTDIR)$(MANDIR)/man5/$(MAN_CONFIG) + +uninstall: deinstall + +clean: + rm -f $(OBJ) $(BIN) imapfilter.core core *.orig *.BAK *~ + +distclean: clean + @if test -f .Makefile; then mv -f .Makefile Makefile; fi diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..cc09d7d --- /dev/null +++ b/NEWS @@ -0,0 +1,192 @@ +IMAPFilter 2.2.3 - 6 Mar 2011 + - Project moved to GitHub. + - The next UID is returned as an additional return value of check_status(). + - All processing methods now return a boolean based on their success. + - Bug fix; a lost connection is now handled better by trying to reconnect. + - Bug fix; in some cases in IDLE a message had arrived but was ignored. + - Bug fix; in some servers the initial IDLE reply wasn't handled correctly. + - Bug fix; typo errors in the documentation. + +IMAPFilter 2.2.2 - 23 Jan 2010 + - Bug fix; a couple of errors in the extending examples file. + +IMAPFilter 2.2.1 - 20 Jan 2010 + - A global option for the IDLE refreshing interval was added. + - Bug fix; more detailed reporting when SSL socket errors occur. + +IMAPFilter 2.2 - 30 Dec 2009 + - Support for combining searching methods in multiple mailboxes at the same + or different accounts and processing of the results in bulk. + - Support for meta-searching that allows searching on the previous searching + results. + - The processing and fetching methods were enhanced to reflect the new + changes and the documentation was updated. + - Global options for the message cache and the certificates were added. + - Bug fix; questions for certificates are not asked while in daemon mode, but + instead an error is printed. + * A different format is used for the returned structures of the searching + methods, due to the introduction of multiple mailbox searching and + meta-searching, and thus any configuration files that rely on them should + be updated. Consequently, the processing and fetching methods have been + also enhanced and the relevant documentation updated, and while these + changes are backwards compatible, an update of the configuration file is + still recommended. + +IMAPFilter 2.1.2 - 3 Dec 2009 + - Bug fix; cache for message parts didn't work correctly. + - Bug fix; documentation error. + +IMAPFilter 2.1.1 - 24 Nov 2009 + - Bug fix; global option timeout and enter_idle() didn't play well together. + +IMAPFilter 2.1 - 23 Nov 2009 + - Support for the IMAP IDLE extension (RFC 2177) through the enter_idle() + method. + - Support for fetching of a message's body structure through the + fetch_structure() method, and of a message's specific body part through the + fetch_parts() method. + - Addition of a global option that controls the character set used for all + the searching methods. + - Bug fix; fetching of non-existent messages. + - Bug fix; no trailing end-of-line characters in the results of + fetch_fields(). + +IMAPFilter 2.0.11 - 20 Sep 2009 + - Bug fix; fetching of messages with empty body. + - Workaround for problematic IMAP server sending non-compliant mailbox status + information. + +IMAPFilter 2.0.10 - 16 Feb 2008 + - Bug fix; failed a great number (tens of thousands) of commands were + exchanged with an IMAP server. + - Bug fix; failed to fetch the body of some messages in some extremely rare + occasions. + - Bug fix; the description for the contain_header() method was clarified. + +IMAPFilter 2.0.9 - 26 Dec 2007 + - Bug fix; the match_*() methods failed to match messages. + - Bug fix; the match_*() methods failed with an error when no messages + matched. + - Bug fix; note added in the documentation about the need to use double + backslashes inside of regular expression patterns. + +IMAPFilter 2.0.8 - 23 Dec 2007 + - Bug fix; on some platforms it is necessary to link against the math library. + +IMAPFilter 2.0.7 - 22 Dec 2007 + - Bug fix; the match_*() methods failed with an error message. + +IMAPFilter 2.0.6 - 7 Oct 2007 + - Bug fix; the search query that was sent with the select_all() method had an + incorrect format and this caused an error in some mail servers. + +IMAPFilter 2.0.5 - 4 Oct 2007 + - Bug fix; an error in the sample extensions file. + - Bug fix; typo errors in the manual page. + - The documentation was updated with details and examples on how to access + mailboxes inside folders. + - Examples were added on how to define composite filters that include + multiple searching rules. + +IMAPFilter 2.0.4 - 27 Sep 2007 + - Bug fix; the send_query() method didn't return the special form of table + that the rest of the searching methods did. + - An additional searching method has been added to search for keyword flags + set. + - A new variable that was added to the Makefile makes it possible to set an + alternative environment for the installation path. + +IMAPFilter 2.0.3 - 27 Jul 2007 + - Bug fix; part of the program's functionality didn't seem to work at all. + (did nothing), due to problem when providing the results from searching + methods to processing methods. + +IMAPFilter 2.0.2 - 30 Jun 2007 + - Bug fix; message cache problem due to non-use of message UIDs. + +IMAPFilter 2.0.1 - 29 Jun 2007 + - Bug fix; character set problem with 1.x configuration files. + - Bug fix; typo errors in the documentation. + +IMAPFilter 2.0 - 27 Jun 2007 + - New, more powerful, feature rich and yet simpler configuration file. + - Easier object oriented view of accounts and mailboxes. + - Simpler approach to filters, with infix logical or/and/not operators. + - No more need to mess with server search queries. + - More and simpler functions instead of few and complicated ones. + - More feature complete interface that can now even manipulate mailboxes. + - Regular expressions integrated into the searching interface. + - Effective caching subsystem when fetching message parts. + - Can still read old version 1.x configuration files for compatibility. + - Lua 5.1 and the PCRE library are now requirements. + * The configuration file format has changed. The new format is not backwards + compatible, and thus it should not be mixed with the old format. + Nevertheless, configuration files that employ the old, and now deprecated, + format can still be read and executed as before. + +IMAPFilter 1.3 - 13 Feb 2007 + - Perl Compatible Regular Expression (PCRE) support. + - Compile against Lua 5.1 by default. + - Bug fix; program fault in some cases and when namespace prefix was empty. + - Bug fix; program fault on some platforms when running in verbose mode. + +IMAPFilter 1.2.2 - 1 Aug 2006 + - Bug fix; a mix up of connections could happen in certain circumstances, + when a hostname and/or username was a prefix of another hostname and/or + username respectively, or when the same hostname and username was used to + connect to a different port. + - Bug fix; the list()/lsub() functions parsed mailboxes/folders whose names + contained spaces incorrectly. + - The list() function now does not return the folder itself, when listing + mailboxes inside a specific folder. + - It is now possible to define new user keywords for messages inside a + mailbox, apart from the standard system flags. + +IMAPFilter 1.2.1 - 9 Mar 2006 + - Buf fix; program fault when using the fetch*() family of functions. + +IMAPFilter 1.2 - 2 Mar 2006 + - IPv6 support. + - Lua 5.1 compatibility. + - Bug fix; handle messages containing binary data. + - Bug fix; problems with CPU utilisation when the inactivity timeout timer + was set. + +IMAPFilter 1.1.1 - 11 Nov 2005 + - Bug fix; minor memory leak. + - Bug fix; on some systems, failure resulted while disconnecting from all the + servers, during the shutdown phase just before exiting. + +IMAPFilter 1.1 - 24 Aug 2005 + - Addition of the list() and lsub() commands, that make it possible to get a + list of the available mailboxes or only of those that are subscribed. + Implementation of the IMAP LIST/LSUB commands, with additional support for + the IMAP CHILDREN (RFC 3348) and IMAP NAMESPACE (RFC 2342) extensions. + - New program option to execute a string from the command line, without + loading a configuration file. + - New program option to enter interactive mode after executing the + configuration file or the command line. + - Servers that reply with multiple SEARCH responses are taken into + consideration. + - Bug fix; failure to parse the response to fetchfast() that some mail + servers sent. + - Bug fix; in some systems and when in debug mode, an empty namespace caused + program fault. + +IMAPFilter 1.0.1 - 22 Aug 2004 + - Bug fix; in some cases processing of messages with an empty body caused + failure. + - Bug fix; an invalid namespace prefix was inserted in mailbox names of some + mail servers. + - Unique message identifiers are now used by default, instead of message + sequence numbers, when accessing messages in a mailbox. + - Sequence set ranges are generated and sent to the mail server instead of + enumerations, when this is possible. + - The client now limits the length of the command lines it generates to + approximately 1000 octets, by splitting the request into multiple commands. + - Systems that have no limit on the number of bytes in a pathname are now + considered. + - Debug files are now written in $HOME/.imapfilter/ instead of /tmp/. + +IMAPFilter 1.0 - 23 May 2004 + - Initial release of IMAPFilter with extension language Lua. diff --git a/README b/README new file mode 100644 index 0000000..d78e639 --- /dev/null +++ b/README @@ -0,0 +1,60 @@ + IMAPFilter + +Description + + IMAPFilter is a mail filtering utility. It connects to remote mail servers + using the Internet Message Access Protocol (IMAP), sends searching queries to + the server and processes mailboxes based on the results. It can be used to + delete, copy, move, flag, etc. messages residing in mailboxes at the same or + different mail servers. The 4rev1 and 4 versions of the IMAP protocol are + supported. + + IMAPFilter uses the Lua programming language as a configuration and extension + language. + + +Website + + http://github.com/lefcha/imapfilter + + +Changes + + All the changes in each new release up to the latest are in the NEWS file. + + +Installation + + Compile time requirements are Lua (5.1 or later), the PCRE library, and + optionally the OpenSSL library (for SSL/TLS and CRAM-MD5 support). + + Optionally, configure installation options using the supplied shell script. + To see usage details: + + ./configure -h + + Compile and install the program: + + make + make install + + +Documentation + + There is detailed description of the command line options in the + imapfilter(1) manual page, and of the configuration file format in the + imapfilter_config(5) manual page. + + There are also more configuration examples in the sample.config.lua file, and + some examples of extensions through Lua in the sample.extend.lua file. + + +License + + Released under the terms and conditions of the MIT/X11 license, included in + the LICENSE file. + + +Authors + + See AUTHORS file. diff --git a/account.lua b/account.lua new file mode 100644 index 0000000..dac0cc9 --- /dev/null +++ b/account.lua @@ -0,0 +1,239 @@ +-- The Account class represents an IMAP account. + +Account = {} +IMAP = Account +imap = Account + +Account._mt = {} +setmetatable(Account, Account._mt) + + +Account._mt.__call = function (self, arg) + _check_required(arg.server, 'string') + _check_required(arg.username, 'string') + _check_optional(arg.password, 'string') + _check_optional(arg.port, 'number') + _check_optional(arg.ssl, 'string') + + local object = {} + + object._type = 'account' + + for key, value in pairs(Account) do + if (type(value) == 'function') then + object[key] = value + end + end + + object._mt = {} + object._mt.__index = object._attach_mailbox + setmetatable(object, object._mt) + + object._imap = arg + + return object +end + + +function Account._login_user(self) + if (self._imap.password == nil) then + self._imap.password = get_password('Enter password for ' .. + self._imap.username .. '@' .. self._imap.server .. ': ') + end + + local r = ifcore.login(self._imap) + + if (r == nil) then + return true + elseif (r == true) then + self._mailbox = nil + return true + elseif (r == false) then + return false + end +end + + +function Account._attach_mailbox(self, mailbox) + self[mailbox] = Mailbox(self, mailbox) + return self[mailbox] +end + +function Account._detach_mailbox(self, mailbox) + self[mailbox] = nil +end + + +function Account.list_all(self, folder, mbox) + _check_optional(folder, 'string') + _check_optional(mbox, 'string') + + if (folder == nil) then + folder = '' + else + if (type(options) == 'table' and options.namespace == true) then + if (folder == '/') then + folder = '' + end + if (folder ~= '') then + folder = folder .. '/' + end + end + end + if (mbox == nil) then + mbox = '%' + end + + if (self._login_user(self) ~= true) then + return + end + + local _, mailboxes, folders = ifcore.list(self._imap, '', folder .. mbox) + + local m = {} + for s in string.gmatch(mailboxes, '%C+') do + table.insert(m, s) + end + + local f = {} + for s in string.gmatch(folders, '%C+') do + if s ~= folder and s ~= folder .. '/' then + table.insert(f, s) + end + end + + return m, f +end + +function Account.list_subscribed(self, folder, mbox) + _check_optional(folder, 'string') + _check_optional(mbox, 'string') + + if (folder == nil) then + folder = '' + else + if (type(options) == 'table' and options.namespace == true) then + if (folder == '/') then + folder = '' + end + if (folder ~= '') then + folder = folder .. '/' + end + end + end + if (mbox == nil) then + mbox = '*' + end + + if (self._login_user(self) ~= true) then + return + end + + local _, mailboxes, folders = ifcore.lsub(self._imap, '', folder .. mbox) + + local m = {} + for s in string.gmatch(mailboxes, '%C+') do + table.insert(m, s) + end + + local f = {} + for s in string.gmatch(folders, '%C+') do + if s ~= folder and s ~= folder .. '/' then + table.insert(f, s) + end + end + + return m, f +end + + +function Account.create_mailbox(self, name) + _check_required(name, 'string') + + if (self._login_user(self) ~= true) then + return + end + + local r = ifcore.create(self._imap, name) + + if (type(options) == 'table' and options.info == true) then + print(string.format("Created mailbox %s@%s/%s.", + self._imap.username, self._imap.server, name)) + end + + return r +end + +function Account.delete_mailbox(self, name) + _check_required(name, 'string') + + if (self._login_user(self) ~= true) then + return + end + + local r = ifcore.delete(self._imap, name) + + if (type(options) == 'table' and options.info == true) then + print(string.format("Deleted mailbox %s@%s/%s.", + self._imap.username, self._imap.server, name)) + end + + return r +end + +function Account.rename_mailbox(self, oldname, newname) + _check_required(oldname, 'string') + _check_required(newname, 'string') + + if (self._login_user(self) ~= true) then + return + end + + local r = ifcore.rename(self._imap, oldname, newname) + + if (type(options) == 'table' and options.info == true) then + print(string.format("Renamed mailbox %s@%s/%s to %s@%s/%s.", + self._imap.username, self._imap.server, oldname, + self._imap.username, self._imap.server, newname)) + end + + return r +end + +function Account.subscribe_mailbox(self, name) + _check_required(name, 'string') + + if (self._login_user(self) ~= true) then + return + end + + local r = ifcore.subscribe(self._imap, name) + + if (type(options) == 'table' and options.info == true) then + print(string.format("Subscribed mailbox %s@%s/%s.", + self._imap.username, self._imap.server, name)) + end + + return r +end + +function Account.unsubscribe_mailbox(self, name) + _check_required(name, 'string') + + if (self._login_user(self) ~= true) then + return + end + + local r = ifcore.unsubscribe(self._imap, name) + + if (type(options) == 'table' and options.info == true) then + print(string.format("Unsubscribed mailbox %s@%s/%s.", + self._imap.username, self._imap.server, name)) + end + + return r +end + + +Account._mt.__index = function () end +Account._mt.__newindex = function () end diff --git a/auth.c b/auth.c new file mode 100644 index 0000000..34808f1 --- /dev/null +++ b/auth.c @@ -0,0 +1,73 @@ +#include +#include + +#include "imapfilter.h" +#include "session.h" + +#ifndef NO_CRAMMD5 +#include +#include + + +/* + * Authenticate to the server with the Challenge-Response Authentication + * Mechanism (CRAM). The authentication type associated with CRAM is + * "CRAM-MD5". + */ +int +auth_cram_md5(session *ssn, const char *user, const char *pass) +{ + int t; + size_t n; + unsigned int i; + unsigned char *chal, *resp, *out, *buf; + unsigned char md[EVP_MAX_MD_SIZE], mdhex[EVP_MAX_MD_SIZE * 2 + 1]; + unsigned int mdlen; + HMAC_CTX hmac; + + if ((t = imap_authenticate(ssn, "CRAM-MD5")) == -1) + return -1; + + if (response_authenticate(ssn, t, &chal) == + STATUS_RESPONSE_CONTINUE) { + n = strlen((char *)(chal)) * 3 / 4 + 1; + resp = (unsigned char *)xmalloc(n * sizeof(char)); + memset(resp, 0, n); + + EVP_DecodeBlock(resp, chal, strlen((char *)(chal))); + + HMAC_Init(&hmac, (const unsigned char *)pass, strlen(pass), + EVP_md5()); + HMAC_Update(&hmac, resp, strlen((char *)(resp))); + HMAC_Final(&hmac, md, &mdlen); + + xfree(chal); + xfree(resp); + + for (i = 0; i < mdlen; i++) + snprintf((char *)(mdhex) + i * 2, mdlen * 2 - i * 2 + 1, + "%02x", md[i]); + mdhex[mdlen * 2] = '\0'; + + n = strlen(user) + 1 + strlen((char *)(mdhex)) + 1; + buf = (unsigned char *)xmalloc(n * sizeof(unsigned char)); + memset(buf, 0, n); + + snprintf((char *)(buf), n, "%s %s", user, mdhex); + + n = (strlen((char *)(buf)) + 3) * 4 / 3 + 1; + out = (unsigned char *)xmalloc(n * sizeof(unsigned char)); + memset(out, 0, n); + + EVP_EncodeBlock(out, buf, strlen((char *)(buf))); + + imap_continuation(ssn, (char *)(out), strlen((char *)(out))); + + xfree(buf); + xfree(out); + } else + return -1; + + return response_authenticate(ssn, t, NULL); +} +#endif /* NO_CRAMMD5 */ diff --git a/auxiliary.lua b/auxiliary.lua new file mode 100644 index 0000000..973e1e5 --- /dev/null +++ b/auxiliary.lua @@ -0,0 +1,66 @@ +-- Miscellaneous auxiliary functions. + +function form_date(days) + _check_required(days, 'number') + + return os.date("%d-%b-%Y", os.time() - days * 60 * 60 * 24) +end + + +function get_password(prompt) + _check_optional(prompt, 'string') + + if (prompt ~= nil) then + io.write(prompt) + else + io.write('Enter password: ') + end + + ifsys.noecho() + local p = io.read() + ifsys.echo() + + return p +end + + +function pipe_to(command, data) + _check_required(command, 'string') + _check_required(data, 'string') + + f = ifsys.popen(command, "w") + + ifsys.write(f, data) + + return ifsys.pclose(f) +end + +function pipe_from(command) + _check_required(command, 'string') + + f = ifsys.popen(command, "r") + + local string = '' + while (true) do + s = ifsys.read(f) + if (s ~= nil) then + string = string .. s + else + break + end + end + + return ifsys.pclose(f), string +end + + +function become_daemon(interval, commands) + _check_required(interval, 'number') + _check_required(commands, 'function') + + ifsys.daemon() + + repeat + pcall(commands) + until (ifsys.sleep(interval) ~= 0) +end diff --git a/buffer.c b/buffer.c new file mode 100644 index 0000000..65a1189 --- /dev/null +++ b/buffer.c @@ -0,0 +1,60 @@ +#include + +#include "imapfilter.h" +#include "buffer.h" + + +/* + * Initialize buffer. + */ +void +buffer_init(buffer *buf, size_t n) +{ + + buf->data = (char *)xmalloc((n + 1) * sizeof(char)); + *buf->data = '\0'; + buf->len = 0; + buf->size = n; +} + + +/* + * Free allocated memory of buffer. + */ +void +buffer_free(buffer *buf) +{ + + if (!buf->data) + return; + + xfree(buf->data); + buf->data = NULL; +} + + +/* + * Reset buffer. + */ +void +buffer_reset(buffer *buf) +{ + + *buf->data = '\0'; + buf->len = 0; +} + + +/* + * Check if the buffer has enough space to store data and reallocate memory if + * needed. + */ +void +buffer_check(buffer *buf, size_t n) +{ + + while (n > buf->size) { + buf->size *= 2; + buf->data = (char *)xrealloc(buf->data, buf->size + 1); + } +} diff --git a/buffer.h b/buffer.h new file mode 100644 index 0000000..a7f7750 --- /dev/null +++ b/buffer.h @@ -0,0 +1,23 @@ +#ifndef BUFFER_H +#define BUFFER_H + + +#include + + +/* Temporary buffer. */ +typedef struct buffer { + char *data; /* Text or binary data. */ + size_t len; /* Length of text or binary data. */ + size_t size; /* Maximum size of data. */ +} buffer; + + +/* buffer.c */ +void buffer_init(buffer *buf, size_t n); +void buffer_free(buffer *buf); +void buffer_reset(buffer *buf); +void buffer_check(buffer *buf, size_t n); + + +#endif /* BUFFER_H */ diff --git a/cert.c b/cert.c new file mode 100644 index 0000000..a427fb8 --- /dev/null +++ b/cert.c @@ -0,0 +1,235 @@ +#ifndef NO_SSLTLS + +#include +#include +#include +#include +#include +#include + +#include "imapfilter.h" +#include "session.h" +#include "pathnames.h" + +#include +#include +#include +#include + + +extern environment env; + + +int check_cert(X509 *pcert, unsigned char *pmd, unsigned int *pmdlen); +void print_cert(X509 *cert, unsigned char *md, unsigned int *mdlen); +int write_cert(X509 *cert); +int mismatch_cert(void); + + +/* + * Get SSL/TLS certificate check it, maybe ask user about it and act + * accordingly. + */ +int +get_cert(session *ssn) +{ + X509 *cert; + unsigned char md[EVP_MAX_MD_SIZE]; + unsigned int mdlen; + + mdlen = 0; + + if (!(cert = SSL_get_peer_certificate(ssn->ssl))) + return -1; + + if (!(X509_digest(cert, EVP_md5(), md, &mdlen))) + return -1; + + switch (check_cert(cert, md, &mdlen)) { + case 0: + if (isatty(STDIN_FILENO) == 0) + fatal(ERROR_CERTIFICATE, "%s\n", + "can't accept certificate in non-interactive mode"); + print_cert(cert, md, &mdlen); + if (write_cert(cert) == -1) + goto fail; + break; + case -1: + if (isatty(STDIN_FILENO) == 0) + fatal(ERROR_CERTIFICATE, "%s\n", + "certificate mismatch in non-interactive mode"); + print_cert(cert, md, &mdlen); + if (mismatch_cert() == -1) + goto fail; + break; + } + + X509_free(cert); + + return 0; + +fail: + X509_free(cert); + + return -1; +} + + +/* + * Check if the SSL/TLS certificate exists in the certificates file. + */ +int +check_cert(X509 *pcert, unsigned char *pmd, unsigned int *pmdlen) +{ + int n, r; + FILE *fd; + char b; + char *certf; + X509 *cert; + unsigned char md[EVP_MAX_MD_SIZE]; + unsigned int mdlen; + + r = 0; + cert = NULL; + + n = snprintf(&b, 1, "%s/%s", env.home, PATHNAME_CERTS); + + if (env.pathmax != -1 && n > env.pathmax) + fatal(ERROR_PATHNAME, + "pathname limit %ld exceeded: %d\n", env.pathmax, n); + + certf = (char *)xmalloc((n + 1) * sizeof(char)); + snprintf(certf, n + 1, "%s/%s", env.home, PATHNAME_CERTS); + + if (!exists_file(certf)) { + xfree(certf); + return 0; + } + + fd = fopen(certf, "r"); + + xfree(certf); + + if (fd == NULL) + return -1; + + while ((cert = PEM_read_X509(fd, &cert, NULL, NULL)) != NULL) { + if (X509_subject_name_cmp(cert, pcert) != 0 || + X509_issuer_name_cmp(cert, pcert) != 0) + continue; + + if (!X509_digest(cert, EVP_md5(), md, &mdlen) || + *pmdlen != mdlen) + continue; + + if (memcmp(pmd, md, mdlen) != 0) { + r = -1; + break; + } + r = 1; + break; + } + + fclose(fd); + X509_free(cert); + + return r; +} + + +/* + * Print information about the SSL/TLS certificate. + */ +void +print_cert(X509 *cert, unsigned char *md, unsigned int *mdlen) +{ + unsigned int i; + char *c; + + c = X509_NAME_oneline(X509_get_subject_name(cert), NULL, 0); + printf("Server certificate subject: %s\n", c); + xfree(c); + + c = X509_NAME_oneline(X509_get_issuer_name(cert), NULL, 0); + printf("Server certificate issuer: %s\n", c); + xfree(c); + + printf("Server key fingerprint: "); + for (i = 0; i < *mdlen; i++) + printf(i != *mdlen - 1 ? "%02X:" : "%02X\n", md[i]); +} + + +/* + * Write the SSL/TLS certificate after asking the user to accept/reject it. + */ +int +write_cert(X509 *cert) +{ + int n; + FILE *fd; + char b, c, buf[64]; + char *certf; + + do { + printf("(R)eject, accept (t)emporarily or " + "accept (p)ermanently? "); + if (fgets(buf, sizeof(buf), stdin) == NULL) + return -1; + c = tolower((int)(*buf)); + } while (c != 'r' && c != 't' && c != 'p'); + + if (c == 'r') + return -1; + else if (c == 't') + return 0; + + n = snprintf(&b, 1, "%s/%s", env.home, PATHNAME_CERTS); + + if (env.pathmax != -1 && n > env.pathmax) + fatal(ERROR_PATHNAME, + "pathname limit %ld exceeded: %d\n", env.pathmax, n); + + certf = (char *)xmalloc((n + 1) * sizeof(char)); + snprintf(certf, n + 1, "%s/%s", env.home, PATHNAME_CERTS); + + create_file(certf, S_IRUSR | S_IWUSR); + + fd = fopen(certf, "a"); + + xfree(certf); + + if (fd == NULL) + return -1; + + PEM_write_X509(fd, cert); + + fclose(fd); + + return 0; +} + + +/* + * Ask user to proceed, while a fingerprint mismatch in the SSL/TLS certificate + * was found. + */ +int +mismatch_cert(void) +{ + char c, buf[64]; + + do { + printf("ATTENTION: SSL/TLS certificate fingerprint mismatch.\n" + "Proceed with the connection (y/n)? "); + if (fgets(buf, sizeof(buf), stdin) == NULL) + return -1; + c = tolower((int)(*buf)); + } while (c != 'y' && c != 'n'); + + if (c == 'y') + return 0; + else + return -1; +} +#endif /* NO_SSLTLS */ diff --git a/common.lua b/common.lua new file mode 100644 index 0000000..356e4b0 --- /dev/null +++ b/common.lua @@ -0,0 +1,510 @@ +-- Common functions for all classes. + +function _check_required(arg, argtype) + if (type(arg) == 'nil') then + error('required argument left out', 3) + else + _check_optional(arg, argtype) + end +end + +function _check_optional(arg, argtype) + if (type(arg) ~= 'nil') then + if (type(argtype) == 'string') then + if (type(arg) ~= argtype) then + error(argtype .. ' argument expected, got ' .. type(arg), 3) + end + elseif (type(argtype) == 'table') then + local b = false + for _, t in ipairs(argtype) do + if (type(arg) == t) then + b = true + end + end + if (b == false) then + error(argtype .. ' argument expected, got ' .. type(arg), 3) + end + end + end +end + + +function _extract_mailboxes(messages) + local t = {} + for _, v in ipairs(messages) do + b, _ = unpack(v) + t[b] = true + end + return t +end + +function _extract_messages(mailbox, messages) + local t = {} + for _, v in ipairs(messages) do + b, m = unpack(v) + if mailbox == b then + table.insert(t, m) + end + end + return t +end + + +function _make_range(messages) + for _, m in ipairs(messages) do + if type(m) ~= 'number' then + return messages + end + end + + table.sort(messages) + + local t = {} + local a, z + for _, m in ipairs(messages) do + if a == nil or z == nil then + a = m + z = m + else + if m == z + 1 then + z = m + else + if a == z then + table.insert(t, tostring(a)) + else + table.insert(t, a .. ':' .. z) + end + a = m + z = m + end + end + end + + if a == z then + table.insert(t, tostring(a)) + else + table.insert(t, a .. ':' .. z) + end + + return t +end + + +function _make_query(criteria) + local s = 'ALL ' + + if (criteria.invert ~= true) then + for ka, va in ipairs(criteria) do + if (type(va) == 'string') then + s = s .. '' .. '(' .. va .. ')' .. ' ' + elseif (type(va) == 'table') then + for i = 1, #va - 1 do + s = s .. 'OR ' + end + for ko, vo in ipairs(va) do + if (type(vo) ~= 'string') then + error('filter rule not a string', 2) + end + s = s .. '(' .. vo .. ') ' + end + else + error('filter element not a string or table', 2) + end + end + else + for i = 1, #criteria - 1 do + s = s .. 'OR ' + end + for ko, vo in ipairs(criteria) do + if (type(vo) == 'string') then + s = s .. '' .. '(' .. vo .. ')' .. ' ' + elseif (type(vo) == 'table') then + s = s .. '(' + for ka, va in ipairs(vo) do + if (type(va) ~= 'string') then + error('filter rule not a string', 2) + end + s = s .. va .. ' ' + end + s = string.gsub(s, '(.+) ', '%1') + s = s .. ') ' + else + error('filter rule not a string or table', 2) + end + end + end + + s = string.gsub(s, '(.+) ', '%1') + + return s +end + + +function _parse_structure(b) + local bs = _parse_body(b) + if not bs then error(b.i .. ':' .. b.s) end + return _parse_normalize(bs) +end + +function _parse_normalize(bs, key, val) + if not key or not val then + if #bs == 0 then + return { ['1'] = bs } + else + for k, v in pairs(bs) do + if type(k) ~= 'number' then bs[k] = nil end + end + for k, v in ipairs(bs) do + _parse_normalize(bs, k, v) + bs[tostring(k)] = v + bs[k] = nil + end + end + return bs + else + for k, v in ipairs(val) do + local new = tostring(key) .. '.' .. tostring(k) + bs[new] = v + _parse_normalize(bs, new, v) + val[k] = nil + end + end +end + +function _parse_body(b) + if not _parse_lpar(b) then return end + + local bp + if _parse_lpar(b, true) then + bp = _parse_mpart(b) + else + bp = _parse_1part(b) + end + + if not _parse_rpar(b) then return end + + return bp +end + +function _parse_1part(b) + local i = b.i + local t = _parse_string(b) + _parse_space(b) + local s = _parse_string(b) + if t and t:lower() == 'message' and s and s:lower() == 'rfc822' then + return _parse_message(b) + else + b.i = i + return _parse_basic(b) + end +end + +function _parse_basic(b) + local bp = {} + local s + + s = _parse_string(b) + if not s then return end + bp['type'] = s + + _parse_space(b) + + s = _parse_string(b) + if not s then return end + bp['type'] = bp['type'] .. '/' .. s + + _parse_space(b) + + s = _parse_param(b, 'name') + if s then bp['name'] = s end + + _parse_space(b) + _parse_nstring(b) + _parse_space(b) + _parse_nstring(b) + _parse_space(b) + _parse_string(b) + _parse_space(b) + bp['size'] = _parse_number(b) + if bp['type']:sub(1, 5):lower() == 'text/' then + _parse_space(b) + _parse_number(b) + end + if _parse_space(b) then _parse_nstring(b) end + if _parse_space(b) then + s = _parse_dsp(b) + if s then bp['name'] = s end + end + if _parse_space(b) then _parse_lang(b) end + if _parse_space(b) then _parse_nstring(b) end + while _parse_space(b) and b.i <= #b.s do _parse_extension(b) end + + return bp +end + +function _parse_mpart(b) + local bp = {} + local i = 1 + local s + + bp['type'] = 'multipart' + + while _parse_lpar(b, true) and b.i <= #b.s do + bp[i] = _parse_body(b) + i = i + 1 + end + + _parse_space(b) + + s = _parse_string(b) + if not s then return end + bp['type'] = bp['type'] .. '/' .. s + + if _parse_space(b) then + s = _parse_param(b, 'name') + if s then bp['name'] = s end + end + if _parse_space(b) then + s = _parse_dsp(b) + if s then bp['name'] = s end + end + if _parse_space(b) then _parse_lang(b) end + if _parse_space(b) then _parse_nstring(b) end + while _parse_space(b) and b.i < #b.s do _parse_extension(b) end + + return bp +end + +function _parse_message(b) + local bp = {} + local s + + bp['type'] = 'message/rfc822' + + _parse_space(b) + + s = _parse_param(b, 'name') + if s then bp['name'] = s end + + _parse_space(b) + _parse_nstring(b) + _parse_space(b) + _parse_nstring(b) + _parse_space(b) + _parse_string(b) + _parse_space(b) + bp['size'] = _parse_number(b) + + _parse_space(b) + _parse_envelope(b) + + _parse_space(b) + local p = _parse_body(b) + if not p then return end + if #p == 0 then + bp[1] = p + else + for k, v in pairs(p) do + if type(k) == 'number' then + bp[k] = v + end + end + end + + _parse_space(b) + _parse_number(b) + + if _parse_space(b) then _parse_nstring(b) end + if _parse_space(b) then + s = _parse_dsp(b) + if s then bp['name'] = s end + end + if _parse_space(b) then _parse_lang(b) end + if _parse_space(b) then _parse_nstring(b) end + while _parse_space(b) and b.i <= #b.s do _parse_extension(b) end + + return bp +end + +function _parse_envelope(b) + _parse_lpar(b) + _parse_nstring(b) + _parse_space(b) + _parse_nstring(b) + _parse_space(b) + _parse_address(b) + _parse_space(b) + _parse_address(b) + _parse_space(b) + _parse_address(b) + _parse_space(b) + _parse_address(b) + _parse_space(b) + _parse_address(b) + _parse_space(b) + _parse_address(b) + _parse_space(b) + _parse_nstring(b) + _parse_space(b) + _parse_nstring(b) + _parse_rpar(b) +end + +function _parse_address(b) + if _parse_lpar(b) then + while _parse_lpar(b) and b.i <= #b.s do + _parse_nstring(b) + _parse_space(b) + _parse_nstring(b) + _parse_space(b) + _parse_nstring(b) + _parse_space(b) + _parse_nstring(b) + _parse_rpar(b) + end + _parse_rpar(b) + elseif _parse_nil(b) then + end +end + +function _parse_lang(b) + if _parse_lpar(b) then + local lang = {} + repeat + table.insert(lang, _parse_string(b)) + until not _parse_space(b) or b.i > #b.s + _parse_rpar(b) + return lang + else + return _parse_nstring(b) + end +end + +function _parse_dsp(b) + local r + if _parse_lpar(b) then + _parse_string(b) + _parse_space(b) + r = _parse_param(b, 'filename') + _parse_rpar(b) + elseif _parse_nil(b) then + end + return r +end + +function _parse_param(b, key) + local r + if _parse_lpar(b) then + repeat + local s = _parse_string(b) + _parse_space(b) + if s and s:lower() == key then + r = _parse_string(b) + else + _parse_string(b) + end + until not _parse_space(b) or b.i > #b.s + _parse_rpar(b) + elseif _parse_nil(b) then + end + return r +end + +function _parse_extension(b) + if _parse_nstring(b) then + elseif _parse_number(b) then + elseif _parse_lpar(b) then + _parse_extension(b) + while _parse_space(b) and b.i <= #b.s do + _parse_extension(b) + end + _parse_rpar(b) + else + end +end + +function _parse_space(b, peek) + if b.s:sub(b.i, b.i) == ' ' then + if not peek then b.i = b.i + 1 end + return true + else + return false + end +end + +function _parse_lpar(b, peek) + if b.s:sub(b.i, b.i) == '(' then + if not peek then b.i = b.i + 1 end + return true + else + return false + end +end + +function _parse_rpar(b, peek) + if b.s:sub(b.i, b.i) == ')' then + if not peek then b.i = b.i + 1 end + return true + else + return false + end +end + +function _parse_nil(b) + if b.s:sub(b.i, b.i + 2):upper() == 'NIL' then + b.i = b.i + 3 + return true + else + return false + end +end + +function _parse_string(b) + local i = b.i + + if b.s:sub(i, i) == '"' then + i = i + 1 + else return end + + local j = i + local n = 0 + while true do + n = b.s:find('"', i + n) + if not n then return end + if b.s:sub(n - 1, n - 1) ~= '\\' then + i = n + 1 + b.i = i + return b.s:sub(j, n - 1) + else return end + end +end + +function _parse_nstring(b) + local i = b.i + + if b.s:sub(i, i) == '"' then + i = i + 1 + elseif _parse_nil(b) then + return 'NIL' + else return end + + local j = i + local n = 0 + while true do + n = b.s:find('"', i + n) + if not n then return end + if b.s:sub(n - 1, n - 1) ~= '\\' then + i = n + 1 + b.i = i + return b.s:sub(j, n - 1) + else return end + end +end + +function _parse_number(b) + local j = b.i + local n = b.s:find('[^0-9]', b.i) + if not n then return end + b.i = n + return tonumber(b.s:sub(j, n - 1)) +end diff --git a/configure b/configure new file mode 100755 index 0000000..6b5b0d5 --- /dev/null +++ b/configure @@ -0,0 +1,251 @@ +#!/bin/sh + +# Default values + +prefix="/usr/local" +bindir="$prefix/bin" +sharedir="$prefix/share/imapfilter" +mandir="$prefix/man" + +ssltls="yes" +crammd5="yes" + +incdirs="-I/usr/local/include" +libdirs="-L/usr/local/lib" + +mycflags="$CFLAGS -Wall -O" +myldflags="$LDFLAGS" + +libs="-lm -llua -lpcre" +libssl="-lssl" +libcrypto="-lcrypto" + +defs="-DMAKEFILE_SHAREDIR='\"\$(SHAREDIR)\"'" + +bin="imapfilter" + + +# Get options and arguments + +while getopts "d:p:b:s:m:o:h" opt +do + case $opt in + d | p) + prefix=$OPTARG + bindir=$prefix/bin + sharedir=$prefix/share/imapfilter + mandir=$prefix/man + ;; + b) + bindir=$OPTARG + ;; + s) + sharedir=$OPTARG + ;; + m) + mandir=$OPTARG + ;; + o) + head=`echo $OPTARG | cut -d= -f1` + body=`echo $OPTARG | cut -d= -f2` + if [ $head = "ssltls" ] + then + if [ $body = "yes" ]; then ssltls="yes" + elif [ $body = "no" ]; then ssltls="no" + fi + elif [ $head = "crammd5" ] + then + if [ $body = "yes" ]; then crammd5="yes" + elif [ $body = "no" ]; then crammd5="no" + fi + fi + ;; + h | *) + cat << EOF +Usage: + configure [-h] [-p prefix] [-b bindir] [-s sharedir] [-m mandir] + [-o option=argument] + +Description: + -h This brief usage and description message. + -p prefix Installation path for program's files [$prefix] + -b bindir Installation path for binaries [$bindir] + -s sharedir Installation path for libraries [$sharedir] + -m mandir Installation path for manual pages [$mandir] + -o option=argument Enabling/disabling of program's compilation options. + +Options: + ssltls Secure Socket Layer and Transport Layer Security \ +[$ssltls] + crammd5 Challenge-Response Authentication Mechanism [$crammd5] +EOF + exit 1 + ;; + esac +done + + +# Print values + +cat << EOF +Installation directory: $prefix +Binaries directory: $bindir +Architecture independent libraries: $sharedir +Manual pages directory: $mandir +Secure Socket Layer and Transport Layer Security: $ssltls +Challenge-Response Authentication Mechanism: $crammd5 +EOF + + +# Defines + +if [ $ssltls = "no" ] +then + defs="$defs -DNO_SSLTLS" +fi + +if [ $crammd5 = "no" ] +then + defs="$defs -DNO_CRAMMD5" +fi + + +# Libraries + +if [ $ssltls = "yes" ] +then + libs="$libs $libssl $libcrypto" +elif [ $crammd5 = "yes" ] +then + libs="$libs $libcrypto" +fi + + +# Binary name + +uname -a | grep -qi cygwin +if [ $? = 0 ] +then + bin="imapfilter.exe" +fi + + +# Backup of original Makefile + +if [ ! -f .Makefile ]; then cp -f Makefile .Makefile; fi + + +# Write Makefile + +mv -f Makefile Makefile~ + +cat > Makefile << EOF +DESTDIR = +BINDIR = $bindir +SHAREDIR = $sharedir +MANDIR = $mandir + +INCDIRS = $incdirs +LIBDIRS = $libdirs + +MYCFLAGS = $mycflags +MYLDFLAGS = $myldflags + +DEFS = $defs + +CFLAGS = \$(MYCFLAGS) \$(DEFS) \$(INCDIRS) +LDFLAGS = \$(MYLDFLAGS) \$(LIBDIRS) + +LIBS = $libs + +MAN_BIN = imapfilter.1 +MAN_CONFIG = imapfilter_config.5 + +COMMON_LUA = common.lua +SET_LUA = set.lua +REGEX_LUA = regex.lua +ACCOUNT_LUA = account.lua +MAILBOX_LUA = mailbox.lua +MESSAGE_LUA = message.lua +OPTIONS_LUA = options.lua +AUXILIARY_LUA = auxiliary.lua +DEPRECATED_LUA = deprecated.lua + +BIN = $bin +OBJ = auth.o buffer.o cert.o core.o file.o imap.o imapfilter.o list.o log.o \\ + lua.o memory.o misc.o namespace.o pcre.o regexp.o request.o \\ + response.o session.o signal.o socket.o system.o + +all: \$(BIN) + +\$(BIN): \$(OBJ) + \$(CC) -o \$(BIN) \$(LDFLAGS) \$(OBJ) \$(LIBS) + +\$(OBJ): imapfilter.h +buffer.o imap.o imapfilter.o namespace.o request.o response.o: buffer.h +cert.o file.o imapfilter.o log.o lua.o: pathnames.h +imapfilter.o log.o session.o: list.h +imapfilter.o regexp.o response.o: regexp.h +auth.o cert.o imap.o imapfilter.o log.o request.o response.o session.o \\ + socket.o: session.h +imapfilter.o: version.h + +install: \$(BIN) + if test ! -d \$(DESTDIR)\$(BINDIR); then \\ + mkdir -p \$(DESTDIR)\$(BINDIR); fi + cp -f \$(BIN) \$(DESTDIR)\$(BINDIR) && \\ + chmod 0755 \$(DESTDIR)\$(BINDIR)/\$(BIN) + if test ! -d \$(DESTDIR)\$(SHAREDIR); then \\ + mkdir -p \$(DESTDIR)\$(SHAREDIR); fi + cp -f \$(COMMON_LUA) \$(DESTDIR)\$(SHAREDIR) && \\ + chmod 0644 \$(DESTDIR)\$(SHAREDIR)/\$(COMMON_LUA) + cp -f \$(SET_LUA) \$(DESTDIR)\$(SHAREDIR) && \\ + chmod 0644 \$(DESTDIR)\$(SHAREDIR)/\$(SET_LUA) + cp -f \$(REGEX_LUA) \$(DESTDIR)\$(SHAREDIR) && \\ + chmod 0644 \$(DESTDIR)\$(SHAREDIR)/\$(REGEX_LUA) + cp -f \$(ACCOUNT_LUA) \$(DESTDIR)\$(SHAREDIR) && \\ + chmod 0644 \$(DESTDIR)\$(SHAREDIR)/\$(ACCOUNT_LUA) + cp -f \$(MAILBOX_LUA) \$(DESTDIR)\$(SHAREDIR) && \\ + chmod 0644 \$(DESTDIR)\$(SHAREDIR)/\$(MAILBOX_LUA) + cp -f \$(MESSAGE_LUA) \$(DESTDIR)\$(SHAREDIR) && \\ + chmod 0644 \$(DESTDIR)\$(SHAREDIR)/\$(MESSAGE_LUA) + cp -f \$(OPTIONS_LUA) \$(DESTDIR)\$(SHAREDIR) && \\ + chmod 0644 \$(DESTDIR)\$(SHAREDIR)/\$(OPTIONS_LUA) + cp -f \$(AUXILIARY_LUA) \$(DESTDIR)\$(SHAREDIR) && \\ + chmod 0644 \$(DESTDIR)\$(SHAREDIR)/\$(AUXILIARY_LUA) + cp -f \$(DEPRECATED_LUA) \$(DESTDIR)\$(SHAREDIR) && \\ + chmod 0644 \$(DESTDIR)\$(SHAREDIR)/\$(DEPRECATED_LUA) + if test ! -d \$(DESTDIR)\$(MANDIR)/man1; then \\ + mkdir -p \$(DESTDIR)\$(MANDIR)/man1; fi + cp -f \$(MAN_BIN) \$(DESTDIR)\$(MANDIR)/man1 && \\ + chmod 0644 \$(DESTDIR)\$(MANDIR)/man1/\$(MAN_BIN) + if test ! -d \$(DESTDIR)\$(MANDIR)/man5; then \\ + mkdir -p \$(DESTDIR)\$(MANDIR)/man5; fi + cp -f \$(MAN_CONFIG) \$(DESTDIR)\$(MANDIR)/man5 && \\ + chmod 0644 \$(DESTDIR)\$(MANDIR)/man5/\$(MAN_CONFIG) + +deinstall: + rm -f \$(DESTDIR)\$(BINDIR)/\$(BIN) \\ + \$(DESTDIR)\$(SHAREDIR)/\$(COMMON_LUA) \\ + \$(DESTDIR)\$(SHAREDIR)/\$(SET_LUA) \\ + \$(DESTDIR)\$(SHAREDIR)/\$(REGEX_LUA) \\ + \$(DESTDIR)\$(SHAREDIR)/\$(ACCOUNT_LUA) \\ + \$(DESTDIR)\$(SHAREDIR)/\$(MAILBOX_LUA) \\ + \$(DESTDIR)\$(SHAREDIR)/\$(MESSAGE_LUA) \\ + \$(DESTDIR)\$(SHAREDIR)/\$(OPTIONS_LUA) \\ + \$(DESTDIR)\$(SHAREDIR)/\$(AUXILIARY_LUA) \\ + \$(DESTDIR)\$(SHAREDIR)/\$(DEPRECATED_LUA) \\ + \$(DESTDIR)\$(MANDIR)/man1/\$(MAN_BIN) \\ + \$(DESTDIR)\$(MANDIR)/man5/\$(MAN_CONFIG) + +uninstall: deinstall + +clean: + rm -f \$(OBJ) \$(BIN) imapfilter.core core *.orig *.BAK *~ + +distclean: clean + @if test -f .Makefile; then mv -f .Makefile Makefile; fi +EOF + + +exit 0 diff --git a/core.c b/core.c new file mode 100644 index 0000000..822af37 --- /dev/null +++ b/core.c @@ -0,0 +1,1169 @@ +#include + +#include +#include +#include + +#include "imapfilter.h" + + +static int ifcore_noop(lua_State *lua); +static int ifcore_login(lua_State *lua); +static int ifcore_logout(lua_State *lua); +static int ifcore_status(lua_State *lua); +static int ifcore_select(lua_State *lua); +static int ifcore_close(lua_State *lua); +static int ifcore_expunge(lua_State *lua); +static int ifcore_search(lua_State *lua); +static int ifcore_list(lua_State *lua); +static int ifcore_lsub(lua_State *lua); +static int ifcore_fetchfast(lua_State *lua); +static int ifcore_fetchflags(lua_State *lua); +static int ifcore_fetchdate(lua_State *lua); +static int ifcore_fetchsize(lua_State *lua); +static int ifcore_fetchheader(lua_State *lua); +static int ifcore_fetchtext(lua_State *lua); +static int ifcore_fetchfields(lua_State *lua); +static int ifcore_fetchstructure(lua_State *lua); +static int ifcore_fetchpart(lua_State *lua); +static int ifcore_store(lua_State *lua); +static int ifcore_copy(lua_State *lua); +static int ifcore_append(lua_State *lua); +static int ifcore_create(lua_State *lua); +static int ifcore_delete(lua_State *lua); +static int ifcore_rename(lua_State *lua); +static int ifcore_subscribe(lua_State *lua); +static int ifcore_unsubscribe(lua_State *lua); +static int ifcore_idle(lua_State *lua); + + +/* Lua imapfilter core library functions. */ +static const luaL_reg ifcorelib[] = { + { "noop", ifcore_noop }, + { "logout", ifcore_logout }, + { "login", ifcore_login }, + { "select", ifcore_select }, + { "create", ifcore_create }, + { "delete", ifcore_delete }, + { "rename", ifcore_rename }, + { "subscribe", ifcore_subscribe }, + { "unsubscribe", ifcore_unsubscribe }, + { "list", ifcore_list }, + { "lsub", ifcore_lsub }, + { "status", ifcore_status }, + { "append", ifcore_append }, + { "close", ifcore_close }, + { "expunge", ifcore_expunge }, + { "search", ifcore_search }, + { "fetchfast", ifcore_fetchfast }, + { "fetchflags", ifcore_fetchflags }, + { "fetchdate", ifcore_fetchdate }, + { "fetchsize", ifcore_fetchsize }, + + /* + * RFC 822: message == header + body + * RFC 3501: body == header + text + * + * RFC 3501 notation is used internally, and RFC 822 notation is used + * for the interface available to the user. + */ + { "fetchheader", ifcore_fetchheader }, + { "fetchbody", ifcore_fetchtext }, + + { "fetchfields", ifcore_fetchfields }, + { "fetchstructure", ifcore_fetchstructure }, + { "fetchpart", ifcore_fetchpart }, + { "store", ifcore_store }, + { "copy", ifcore_copy }, + { "idle", ifcore_idle }, + { NULL, NULL } +}; + + +#define DISCOVER_PORT(P, S) ((P) ? (P) : (!(S) ? "143" : "993")) + + +/* + * Core function to reset any inactivity autologout timer on the server. + */ +static int +ifcore_noop(lua_State *lua) +{ + const char *s, *u, *p; + int r; + + if (lua_gettop(lua) != 1) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + + r = request_noop(s, p, u); + + lua_pop(lua, 1); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + return 1; +} + + +/* + * Core function to login to the server. + */ +static int +ifcore_login(lua_State *lua) +{ + const char *s, *u, *w, *p; + int r; + + if (lua_gettop(lua) != 1) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + if (!(w = get_table_string("password"))) + luaL_error(lua, "no password specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + + r = request_login(s, p, get_table_string("ssl"), u, w); + + lua_pop(lua, 1); + + if (r == STATUS_RESPONSE_NONE) + return 0; + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK || + r == STATUS_RESPONSE_PREAUTH)); + + return 1; +} + + +/* + * Core function to logout from the server. + */ +static int +ifcore_logout(lua_State *lua) +{ + const char *s, *u, *p; + int r; + + if (lua_gettop(lua) != 1) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + + r = request_logout(s, p, u); + + lua_pop(lua, 1); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + return 1; +} + + +/* + * Core function to get the status of a mailbox. + */ +static int +ifcore_status(lua_State *lua) +{ + const char *s, *u, *p; + int r; + unsigned int exists, recent, unseen, uidnext; + + exists = recent = unseen = uidnext = -1; + + if (lua_gettop(lua) != 2) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + luaL_checktype(lua, 2, LUA_TSTRING); + + lua_pushvalue(lua, 1); + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + lua_pop(lua, 1); + + r = request_status(s, p, u, lua_tostring(lua, 2), &exists, &recent, + &unseen, &uidnext); + + lua_pop(lua, 2); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + lua_pushnumber(lua, (lua_Number) (exists)); + lua_pushnumber(lua, (lua_Number) (recent)); + lua_pushnumber(lua, (lua_Number) (unseen)); + lua_pushnumber(lua, (lua_Number) (uidnext)); + + return 5; +} + + +/* + * Core function to select a mailbox. + */ +static int +ifcore_select(lua_State *lua) +{ + const char *s, *u, *p; + int r; + + if (lua_gettop(lua) != 2) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + luaL_checktype(lua, 2, LUA_TSTRING); + + lua_pushvalue(lua, 1); + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + lua_pop(lua, 1); + + r = request_select(s, p, u, lua_tostring(lua, 2)); + + lua_pop(lua, 2); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + return 1; +} + + +/* + * Core function to close a mailbox. + */ +static int +ifcore_close(lua_State *lua) +{ + const char *s, *u, *p; + int r; + + if (lua_gettop(lua) != 1) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + + r = request_close(s, p, u); + + lua_pop(lua, 1); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + return 1; +} + + +/* + * Core function to expunge a mailbox. + */ +static int +ifcore_expunge(lua_State *lua) +{ + const char *s, *u, *p; + int r; + + if (lua_gettop(lua) != 1) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + + r = request_expunge(s, p, u); + + lua_pop(lua, 1); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + return 1; +} + + +/* + * Core function to list available mailboxes. + */ +static int +ifcore_list(lua_State *lua) +{ + const char *s, *u, *p; + int r; + char *mboxs, *folders; + + mboxs = folders = NULL; + + if (lua_gettop(lua) != 3) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + luaL_checktype(lua, 2, LUA_TSTRING); + luaL_checktype(lua, 3, LUA_TSTRING); + + lua_pushvalue(lua, 1); + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + lua_pop(lua, 1); + + r = request_list(s, p, u, lua_tostring(lua, 2), lua_tostring(lua, 3), + &mboxs, &folders); + + lua_pop(lua, 3); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + if (!mboxs && !folders) + return 1; + + lua_pushstring(lua, mboxs); + lua_pushstring(lua, folders); + + xfree(mboxs); + xfree(folders); + + return 3; +} + + +/* + * Core function to list subscribed mailboxes. + */ +static int +ifcore_lsub(lua_State *lua) +{ + const char *s, *u, *p; + int r; + char *mboxs, *folders; + + mboxs = folders = NULL; + + if (lua_gettop(lua) != 3) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + luaL_checktype(lua, 2, LUA_TSTRING); + luaL_checktype(lua, 3, LUA_TSTRING); + + lua_pushvalue(lua, 1); + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + lua_pop(lua, 1); + + r = request_lsub(s, p, u, lua_tostring(lua, 2), lua_tostring(lua, 3), + &mboxs, &folders); + + lua_pop(lua, 3); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + if (!mboxs) + return 1; + + lua_pushstring(lua, mboxs); + lua_pushstring(lua, folders); + + xfree(mboxs); + + return 3; +} + + +/* + * Core function to search the messages of a mailbox. + */ +static int +ifcore_search(lua_State *lua) +{ + const char *s, *u, *p; + int r; + char *mesgs; + + mesgs = NULL; + + if (lua_gettop(lua) != 3) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + luaL_checktype(lua, 2, LUA_TSTRING); + luaL_checktype(lua, 3, LUA_TSTRING); + + lua_pushvalue(lua, 1); + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + lua_pop(lua, 1); + + r = request_search(s, p, u, lua_tostring(lua, 2), lua_tostring(lua, 3), + &mesgs); + + lua_pop(lua, 3); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + if (!mesgs) + return 1; + + lua_pushstring(lua, mesgs); + + xfree(mesgs); + + return 2; +} + + +/* + * Core function to fetch message information (flags, date, size). + */ +static int +ifcore_fetchfast(lua_State *lua) +{ + const char *s, *u, *p; + int r; + char *flags, *date, *size; + + flags = date = size = NULL; + + if (lua_gettop(lua) != 2) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + luaL_checktype(lua, 2, LUA_TSTRING); + + lua_pushvalue(lua, 1); + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + lua_pop(lua, 1); + + r = request_fetchfast(s, p, u, lua_tostring(lua, 2), &flags, &date, + &size); + + lua_pop(lua, 2); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + if (!flags || !date || !size) + return 1; + + lua_pushstring(lua, flags); + lua_pushstring(lua, date); + lua_pushstring(lua, size); + + xfree(flags); + xfree(date); + xfree(size); + + return 4; +} + + +/* + * Core function to fetch message flags. + */ +static int +ifcore_fetchflags(lua_State *lua) +{ + const char *s, *u, *p; + int r; + char *flags; + + flags = NULL; + + if (lua_gettop(lua) != 2) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + luaL_checktype(lua, 2, LUA_TSTRING); + + lua_pushvalue(lua, 1); + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + lua_pop(lua, 1); + + r = request_fetchflags(s, p, u, lua_tostring(lua, 2), &flags); + + lua_pop(lua, 2); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + if (!flags) + return 1; + + lua_pushstring(lua, flags); + + xfree(flags); + + return 2; +} + + +/* + * Core function to fetch message date. + */ +static int +ifcore_fetchdate(lua_State *lua) +{ + const char *s, *u, *p; + int r; + char *date; + + date = NULL; + + if (lua_gettop(lua) != 2) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + luaL_checktype(lua, 2, LUA_TSTRING); + + lua_pushvalue(lua, 1); + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + lua_pop(lua, 1); + + r = request_fetchdate(s, p, u, lua_tostring(lua, 2), &date); + + lua_pop(lua, 2); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + if (!date) + return 1; + + lua_pushstring(lua, date); + + xfree(date); + + return 2; +} + + +/* + * Core function to fetch message size. + */ +static int +ifcore_fetchsize(lua_State *lua) +{ + const char *s, *u, *p; + int r; + char *size; + + size = NULL; + + if (lua_gettop(lua) != 2) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + luaL_checktype(lua, 2, LUA_TSTRING); + + lua_pushvalue(lua, 1); + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + lua_pop(lua, 1); + + r = request_fetchsize(s, p, u, lua_tostring(lua, 2), &size); + + lua_pop(lua, 2); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + if (!size) + return 1; + + lua_pushstring(lua, size); + + xfree(size); + + return 2; +} + + +/* + * Core function to fetch message body structure. + */ +static int +ifcore_fetchstructure(lua_State *lua) +{ + const char *s, *u, *p; + int r; + char *structure; + + structure = NULL; + + if (lua_gettop(lua) != 2) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + luaL_checktype(lua, 2, LUA_TSTRING); + + lua_pushvalue(lua, 1); + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + lua_pop(lua, 1); + + r = request_fetchstructure(s, p, u, lua_tostring(lua, 2), &structure); + + lua_pop(lua, 3); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + if (!structure) + return 1; + + lua_pushstring(lua, structure); + + return 2; +} + + +/* + * Core function to fetch message header. + */ +static int +ifcore_fetchheader(lua_State *lua) +{ + const char *s, *u, *p; + int r; + char *header; + size_t len; + + header = NULL; + len = 0; + + if (lua_gettop(lua) != 2) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + luaL_checktype(lua, 2, LUA_TSTRING); + + lua_pushvalue(lua, 1); + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + lua_pop(lua, 1); + + r = request_fetchheader(s, p, u, lua_tostring(lua, 2), &header, &len); + + lua_pop(lua, 2); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + if (!header) + return 1; + + lua_pushlstring(lua, header, len); + + return 2; +} + + +/* + * Core function to fetch message text. + */ +static int +ifcore_fetchtext(lua_State *lua) +{ + const char *s, *u, *p; + int r; + char *text; + size_t len; + + text = NULL; + len = 0; + + if (lua_gettop(lua) != 2) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + luaL_checktype(lua, 2, LUA_TSTRING); + + lua_pushvalue(lua, 1); + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + lua_pop(lua, 1); + + r = request_fetchtext(s, p, u, lua_tostring(lua, 2), &text, &len); + + lua_pop(lua, 2); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + if (!text) + return 1; + + lua_pushlstring(lua, text, len); + + return 2; +} + + +/* + * Core function to fetch message specific header fields. + */ +static int +ifcore_fetchfields(lua_State *lua) +{ + const char *s, *u, *p; + int r; + char *fields; + size_t len; + + fields = NULL; + len = 0; + + if (lua_gettop(lua) != 3) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + luaL_checktype(lua, 2, LUA_TSTRING); + luaL_checktype(lua, 3, LUA_TSTRING); + + lua_pushvalue(lua, 1); + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + lua_pop(lua, 1); + + r = request_fetchfields(s, p, u, lua_tostring(lua, 2), + lua_tostring(lua, 3), &fields, &len); + + lua_pop(lua, 3); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + if (!fields) + return 1; + + lua_pushlstring(lua, fields, len); + + return 2; +} + + +/* + * Core function to fetch message specific part. + */ +static int +ifcore_fetchpart(lua_State *lua) +{ + const char *s, *u, *p; + int r; + char *part; + size_t len; + + part = NULL; + len = 0; + + if (lua_gettop(lua) != 3) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + luaL_checktype(lua, 2, LUA_TSTRING); + luaL_checktype(lua, 3, LUA_TSTRING); + + lua_pushvalue(lua, 1); + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + lua_pop(lua, 1); + + r = request_fetchpart(s, p, u, lua_tostring(lua, 2), + lua_tostring(lua, 3), &part, &len); + + lua_pop(lua, 3); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + if (!part) + return 1; + + lua_pushlstring(lua, part, len); + + return 2; +} + + +/* + * Core function to change message flags. + */ +static int +ifcore_store(lua_State *lua) +{ + const char *s, *u, *p; + int r; + + if (lua_gettop(lua) != 4) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + luaL_checktype(lua, 2, LUA_TSTRING); + luaL_checktype(lua, 3, LUA_TSTRING); + luaL_checktype(lua, 4, LUA_TSTRING); + + lua_pushvalue(lua, 1); + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + lua_pop(lua, 1); + + r = request_store(s, p, u, lua_tostring(lua, 2), lua_tostring(lua, 3), + lua_tostring(lua, 4)); + + lua_pop(lua, 4); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + return 1; +} + + +/* + * Core function to copy messages between mailboxes. + */ +static int +ifcore_copy(lua_State *lua) +{ + const char *s, *u, *p; + int r; + + if (lua_gettop(lua) != 3) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + luaL_checktype(lua, 2, LUA_TSTRING); + luaL_checktype(lua, 3, LUA_TSTRING); + + lua_pushvalue(lua, 1); + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + lua_pop(lua, 1); + + r = request_copy(s, p, u, lua_tostring(lua, 2), lua_tostring(lua, 3)); + + lua_pop(lua, 3); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + return 1; +} + + +/* + * Core function to append messages to a mailbox. + */ +static int +ifcore_append(lua_State *lua) +{ + const char *s, *u, *p; + int r; + + switch (lua_gettop(lua)) { + case 5: + luaL_checktype(lua, 5, LUA_TSTRING); + /* FALLTHROUGH */ + case 4: + luaL_checktype(lua, 4, LUA_TSTRING); + /* FALLTHROUGH */ + case 3: + luaL_checktype(lua, 3, LUA_TSTRING); + luaL_checktype(lua, 2, LUA_TSTRING); + luaL_checktype(lua, 1, LUA_TTABLE); + break; + default: + luaL_error(lua, "wrong number of arguments"); + break; + } + + lua_pushvalue(lua, 1); + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + lua_pop(lua, 1); + + r = request_append(s, p, u, lua_tostring(lua, 2), lua_tostring(lua, 3), + lua_strlen(lua, 3), lua_tostring(lua, 4), lua_tostring(lua, 5)); + + lua_pop(lua, lua_gettop(lua)); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + return 1; +} + + +/* + * Core function to create a mailbox. + */ +static int +ifcore_create(lua_State *lua) +{ + const char *s, *u, *p; + int r; + + if (lua_gettop(lua) != 2) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + luaL_checktype(lua, 2, LUA_TSTRING); + + lua_pushvalue(lua, 1); + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + lua_pop(lua, 1); + + r = request_create(s, p, u, lua_tostring(lua, 2)); + + lua_pop(lua, 2); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + return 1; +} + + +/* + * Core function to delete a mailbox. + */ +static int +ifcore_delete(lua_State *lua) +{ + const char *s, *u, *p; + int r; + + if (lua_gettop(lua) != 2) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + luaL_checktype(lua, 2, LUA_TSTRING); + + lua_pushvalue(lua, 1); + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + lua_pop(lua, 1); + + r = request_delete(s, p, u, lua_tostring(lua, 2)); + + lua_pop(lua, 2); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + return 1; +} + + +/* + * Core function to rename a mailbox. + */ +static int +ifcore_rename(lua_State *lua) +{ + const char *s, *u, *p; + int r; + + if (lua_gettop(lua) != 3) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + luaL_checktype(lua, 2, LUA_TSTRING); + luaL_checktype(lua, 3, LUA_TSTRING); + + lua_pushvalue(lua, 1); + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + lua_pop(lua, 1); + + r = request_rename(s, p, u, lua_tostring(lua, 2), lua_tostring(lua, 3)); + + lua_pop(lua, 3); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + return 1; +} + + +/* + * Core function to subscribe a mailbox. + */ +static int +ifcore_subscribe(lua_State *lua) +{ + const char *s, *u, *p; + int r; + + if (lua_gettop(lua) != 2) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + luaL_checktype(lua, 2, LUA_TSTRING); + + lua_pushvalue(lua, 1); + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + lua_pop(lua, 1); + + r = request_subscribe(s, p, u, lua_tostring(lua, 2)); + + lua_pop(lua, 2); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + return 1; +} + + +/* + * Core function to unsubscribe a mailbox. + */ +static int +ifcore_unsubscribe(lua_State *lua) +{ + const char *s, *u, *p; + int r; + + if (lua_gettop(lua) != 2) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + luaL_checktype(lua, 2, LUA_TSTRING); + + lua_pushvalue(lua, 1); + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + lua_pop(lua, 1); + + r = request_unsubscribe(s, p, u, lua_tostring(lua, 2)); + + lua_pop(lua, 2); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + return 1; +} + + +/* + * Core function to go to idle state. + */ +static int +ifcore_idle(lua_State *lua) +{ + const char *s, *u, *p; + int r; + + if (lua_gettop(lua) != 1) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TTABLE); + + if (!(s = get_table_string("server"))) + luaL_error(lua, "no mail server specified"); + if (!(u = get_table_string("username"))) + luaL_error(lua, "no username specified"); + p = DISCOVER_PORT(get_table_string("port"), get_table_string("ssl")); + + r = request_idle(s, p, u); + + lua_pop(lua, 1); + + lua_pushboolean(lua, (r == STATUS_RESPONSE_OK)); + + return 1; +} + + +/* + * Open imapfilter core library. + */ +LUALIB_API int +luaopen_ifcore(lua_State *lua) +{ + + luaL_register(lua, "ifcore", ifcorelib); + + return 1; +} diff --git a/deprecated.lua b/deprecated.lua new file mode 100644 index 0000000..3ba7bb8 --- /dev/null +++ b/deprecated.lua @@ -0,0 +1,609 @@ +-- The old and deprecated interface, provided for compatibility. + +function check(account, mbox) + _check_required(account, 'table') + _check_required(mbox, 'string') + + _check_required(account.server, 'string') + _check_required(account.username, 'string') + _check_required(account.password, 'string') + + if (ifcore.login(account) ~= true) then + return + end + + local _, exist, recent, unseen = ifcore.status(account, mbox) + + if (type(options) == 'table' and options.info == true) then + print(string.format("%d messages, %d recent, %d unseen, in %s@%s/%s.", + exist, recent, unseen, account.username, + account.server, mbox)) + end + + return exist, recent, unseen +end + + +function match(account, mbox, criteria) + _check_required(account, 'table') + _check_required(mbox, 'string') + _check_required(criteria, 'table') + + _check_required(account.server, 'string') + _check_required(account.username, 'string') + _check_required(account.password, 'string') + + if (ifcore.login(account) ~= true) then + return + end + + if (_cached_select(account, mbox) ~= true) then + return {} + end + + local charset = '' + if (type(options) == 'table' and type(options.charset) == 'string') then + charset = options.charset + end + + local _, results = ifcore.search(account, _make_query(criteria), charset) + + if (type(options) == 'table' and options.close == true) then + _cached_close(account) + end + + if (results == nil) then + return {} + end + + local t = {} + for n in string.gmatch(results, '%d+') do + table.insert(t, tonumber(n)) + end + + return t +end + + +function flag(account, mbox, mode, flags, messages) + _check_required(account, 'table') + _check_required(mbox, 'string') + _check_required(mode, 'string') + _check_required(flags, 'table') + _check_required(messages, 'table') + + _check_required(account.server, 'string') + _check_required(account.username, 'string') + _check_required(account.password, 'string') + + local r = flag_aux(account, mbox, mode, flags, messages) + + if (type(options) == 'table' and options.info == true and + messages ~= nil and r == true) then + print(string.format("%d messages flagged in %s@%s/%s.", + #messages, account.username, + account.server, mbox)) + end + + return r +end + +function flag_aux(account, mbox, mode, flags, messages) + if (#messages == 0) then + return + end + + if (mode ~= 'add' and mode ~= 'remove' and mode ~= 'replace') then + error('"add", "remove" or "replace" expected for mode', 3) + end + + + if (ifcore.login(account) ~= true) then + return + end + + if (_cached_select(account, mbox) ~= true) then + return + end + + local f = '' + if (#flags ~= 0) then + if (flags.keywords ~= true) then + for k, v in ipairs(flags) do + if (string.lower(v) == 'answered' or + string.lower(v) == 'deleted' or + string.lower(v) == 'draft' or + string.lower(v) == 'flagged' or + string.lower(v) == 'seen') then + f = f .. '\\' .. v .. ' ' + end + end + f = string.gsub(f, '(.+) ', '%1') + else + f = table.concat(flags, ' ') + end + end + + local m = _make_range(messages) + n = #m + local r = false + for i = 1, n, 50 do + j = i + 49 + if (n < j) then + j = n + end + r = ifcore.store(account, table.concat(m, ',', i, j), mode, f) + if r == false then + break + end + end + + if (type(options) == 'table' and options.close == true) then + _cached_close(account) + end + + return r +end + + +function copy(srcaccount, srcmbox, dstaccount, dstmbox, messages) + _check_required(srcaccount, 'table') + _check_required(srcmbox, 'string') + _check_required(dstaccount, 'table') + _check_required(dstmbox, 'string') + _check_required(messages, 'table') + + _check_required(srcaccount.server, 'string') + _check_required(srcaccount.username, 'string') + _check_required(srcaccount.password, 'string') + + _check_required(dstaccount.server, 'string') + _check_required(dstaccount.username, 'string') + _check_required(dstaccount.password, 'string') + + local r = copy_aux(srcaccount, srcmbox, dstaccount, dstmbox, messages) + + if (type(options) == 'table' and options.info == true and + messages ~= nil and r == true) then + print(string.format("%d messages copied from %s@%s/%s to %s@%s/%s.", + #messages, srcaccount.username, + srcaccount.server, srcmbox, dstaccount.username, + dstaccount.server, dstmbox)) + end + + return r +end + +function copy_aux(srcaccount, srcmbox, dstaccount, dstmbox, messages) + if (#messages == 0) then + return + end + + if (ifcore.login(srcaccount) ~= true) then + return + end + + local r = false + if (srcaccount == dstaccount) then + if (_cached_select(srcaccount, srcmbox) ~= true) then + return + end + + local m = _make_range(messages) + n = #m + for i = 1, n, 50 do + j = i + 49 + if (n < j) then + j = n + end + r = ifcore.copy(srcaccount, table.concat(m, ',', i, j), dstmbox) + if r == false then + break + end + end + + if (type(options) == 'table' and options.close == true) then + _cached_close(srcaccount) + end + else + local fast = fetchfast(srcaccount, srcmbox, messages) + local msgs = fetchmessage(srcaccount, srcmbox, messages) + + if (ifcore.login(dstaccount) ~= true) then + return + end + + for i in pairs(fast) do + for k, v in ipairs(fast[i]['flags']) do + if (string.lower(v) == '\\recent') then + table.remove(fast[i]['flags'], k) + end + end + + r = ifcore.append(dstaccount, dstmbox, msgs[i], + table.concat(fast[i]['flags'], ' '), fast[i]['date']) + end + end + + return r +end + + +function delete(account, mbox, messages) + _check_required(account, 'table') + _check_required(mbox, 'string') + _check_required(messages, 'table') + + _check_required(account.server, 'string') + _check_required(account.username, 'string') + _check_required(account.password, 'string') + + local r = flag_aux(account, mbox, 'add', { 'Deleted' }, messages) + + if (type(options) == 'table' and options.info == true and + messages ~= nil and r == true) then + print(string.format("%d messages deleted in %s@%s/%s.", + #messages, account.username, + account.server, mbox)) + end + + return r +end + + +function move(srcaccount, srcmbox, dstaccount, dstmbox, messages) + _check_required(srcaccount, 'table') + _check_required(srcmbox, 'string') + _check_required(dstaccount, 'table') + _check_required(dstmbox, 'string') + _check_required(messages, 'table') + + _check_required(srcaccount.server, 'string') + _check_required(srcaccount.username, 'string') + _check_required(srcaccount.password, 'string') + + _check_required(dstaccount.server, 'string') + _check_required(dstaccount.username, 'string') + _check_required(dstaccount.password, 'string') + + local rc = copy_aux(srcaccount, srcmbox, dstaccount, dstmbox, messages) + local rf = false + if (rc == true) then + rf = flag_aux(srcaccount, srcmbox, 'add', { 'Deleted' }, messages) + end + + if (type(options) == 'table' and options.info == true and + messages ~= nil and rc == true and rf == true) then + print(string.format("%d messages moved from %s@%s/%s to %s@%s/%s.", + #messages, srcaccount.username, + srcaccount.server, srcmbox, dstaccount.username, + dstaccount.server, dstmbox)) + end + + return rc == true and rf == true +end + + +function fetchheader(account, mbox, messages) + _check_required(account, 'table') + _check_required(mbox, 'string') + _check_required(messages, 'table') + + _check_required(account.server, 'string') + _check_required(account.username, 'string') + _check_required(account.password, 'string') + + if (#messages == 0) then + return + end + + if (ifcore.login(account) ~= true) then + return + end + + if (_cached_select(account, mbox) ~= true) then + return + end + + local results = {} + for i, v in ipairs(messages) do + local _, header = ifcore.fetchheader(account, tostring(v)) + + if (header ~= nil) then + results[tonumber(v)] = header + end + end + + if (type(options) == 'table' and options.close == true) then + _cached_close(account) + end + + return results +end + +function fetchbody(account, mbox, messages) + _check_required(account, 'table') + _check_required(mbox, 'string') + _check_required(messages, 'table') + + _check_required(account.server, 'string') + _check_required(account.username, 'string') + _check_required(account.password, 'string') + + if (#messages == 0) then + return + end + + if (ifcore.login(account) ~= true) then + return + end + + if (_cached_select(account, mbox) ~= true) then + return + end + + local results = {} + for i, v in ipairs(messages) do + local _, body = ifcore.fetchbody(account, tostring(v)) + + if (body ~= nil) then + results[tonumber(v)] = body + end + end + + if (type(options) == 'table' and options.close == true) then + _cached_close(account) + end + + return results +end + +function fetchmessage(account, mbox, messages) + _check_required(account, 'table') + _check_required(mbox, 'string') + _check_required(messages, 'table') + + _check_required(account.server, 'string') + _check_required(account.username, 'string') + _check_required(account.password, 'string') + + if (#messages == 0) then + return + end + + if (ifcore.login(account) ~= true) then + return + end + + if (_cached_select(account, mbox) ~= true) then + return + end + + local results = {} + for i, v in ipairs(messages) do + local _, header = ifcore.fetchheader(account, tostring(v)) + local _, body = ifcore.fetchbody(account, tostring(v)) + + if (header ~= nil and body ~= nil) then + results[tonumber(v)] = header .. body + end + end + + if (type(options) == 'table' and options.close == true) then + _cached_close(account) + end + + return results +end + +fetchtext = fetchmessage + +function fetchfields(account, mbox, fields, messages) + _check_required(account, 'table') + _check_required(mbox, 'string') + _check_required(fields, 'table') + _check_required(messages, 'table') + + _check_required(account.server, 'string') + _check_required(account.username, 'string') + _check_required(account.password, 'string') + + if (#messages == 0) then + return + end + + if (ifcore.login(account) ~= true) then + return + end + + if (_cached_select(account, mbox) ~= true) then + return + end + + local results = {} + for i, v in ipairs(messages) do + local _, headerfields = ifcore.fetchfields(account, tostring(v), + table.concat(fields, ' ')) + + if (headerfields ~= nil) then + results[tonumber(v)] = headerfields + end + end + + if (type(options) == 'table' and options.close == true) then + _cached_close(account) + end + + return results +end + +fetchheaders = fetchfields + +function fetchfast(account, mbox, messages) + _check_required(account, 'table') + _check_required(mbox, 'string') + _check_required(messages, 'table') + + _check_required(account.server, 'string') + _check_required(account.username, 'string') + _check_required(account.password, 'string') + + if (#messages == 0) then + return + end + + if (ifcore.login(account) ~= true) then + return + end + + if (_cached_select(account, mbox) ~= true) then + return + end + + local results = {} + for i, v in ipairs(messages) do + local _, flags, date, size = ifcore.fetchfast(account, tostring(v)) + if (flags ~= nil and date ~= nil and size ~= nil ) then + local f = {} + for s in string.gmatch(flags, '%S+') do + table.insert(f, s) + end + results[tonumber(v)] = {} + results[tonumber(v)]['flags'] = f + results[tonumber(v)]['date'] = date + results[tonumber(v)]['size'] = size + end + end + + if (type(options) == 'table' and options.close == true) then + _cached_close(account) + end + + return results +end + + +function list(account, name) + _check_required(account, 'table') + _check_optional(name, 'string') + + _check_required(account.server, 'string') + _check_required(account.username, 'string') + _check_required(account.password, 'string') + + if (name == nil) then + name = '' + else + if (type(options) == 'table' and options.namespace == true) then + if (name == '/') then + name = '' + end + if (name ~= '') then + name = name .. '/' + end + end + end + + if (ifcore.login(account) ~= true) then + return + end + + local _, mboxs, folders = ifcore.list(account, '', name .. '%') + + local m = {} + for s in string.gmatch(mboxs, '%C+') do + table.insert(m, s) + end + + local f = {} + for s in string.gmatch(folders, '%C+') do + if s ~= name and s ~= name .. '/' then + table.insert(f, s) + end + end + + return m, f +end + +function lsub(account, name) + _check_required(account, 'table') + _check_optional(name, 'string') + + _check_required(account.server, 'string') + _check_required(account.username, 'string') + _check_required(account.password, 'string') + + if (name == nil) then + name = '' + else + if (type(options) == 'table' and options.namespace == true) then + if (name == '/') then + name = '' + end + if (name ~= '') then + name = name .. '/' + end + end + end + + if (ifcore.login(account) ~= true) then + return + end + + local _, mboxs, folders = ifcore.lsub(account, '', name .. '%') + + local m = {} + for s in string.gmatch(mboxs, '%C+') do + table.insert(m, s) + end + + local f = {} + for s in string.gmatch(folders, '%C+') do + if s ~= name and s ~= name .. '/' then + table.insert(f, s) + end + end + + return m, f +end + + +function ping(account) + _check_required(account, 'table') + + _check_required(account.server, 'string') + _check_required(account.username, 'string') + _check_required(account.password, 'string') + + if (ifcore.login(account) ~= true) then + return + end + + local r = ifcore.noop(account) + + return r +end + + +function _cached_select(account, mbox) + if (account.mailbox == nil or account.mailbox ~= mbox) then + if (ifcore.select(account, mbox) == true) then + account.mailbox = mbox + return true + else + return false + end + else + return true + end +end + +function _cached_close(account) + account.mailbox = nil + return ifcore.close(account) +end + + +date_before = form_date +get_pass = get_password +daemon_mode = become_daemon diff --git a/file.c b/file.c new file mode 100644 index 0000000..5d47517 --- /dev/null +++ b/file.c @@ -0,0 +1,132 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "imapfilter.h" +#include "pathnames.h" + + +extern environment env; + + +/* + * Create imapfilter's home directory. + */ +int +create_homedir(void) +{ + int n; + char b; + char *hd; + + n = snprintf(&b, 1, "%s/%s", env.home, PATHNAME_HOME); + + if (env.pathmax != -1 && n > env.pathmax) + fatal(ERROR_PATHNAME, + "pathname limit %ld exceeded: %d\n", env.pathmax, n); + + hd = (char *)xmalloc((n + 1) * sizeof(char)); + snprintf(hd, n + 1, "%s/%s", env.home, PATHNAME_HOME); + + if (!exists_dir(hd)) { + if (mkdir(hd, S_IRUSR | S_IWUSR | S_IXUSR)) + error("could not create directory %s; %s\n", hd, + strerror(errno)); + } + xfree(hd); + + return 0; +} + + +/* + * Check if a file exists. + */ +int +exists_file(char *fname) +{ + struct stat fs; + + if (access(fname, F_OK)) + return 0; + + stat(fname, &fs); + if (!S_ISREG(fs.st_mode)) { + error("file %s not a regular file\n", fname); + return -1; + } + + return 1; +} + + +/* + * Check if a directory exists. + */ +int +exists_dir(char *dname) +{ + struct stat ds; + + if (access(dname, F_OK)) + return 0; + + stat(dname, &ds); + if (!S_ISDIR(ds.st_mode)) { + error("file %s not a directory\n", dname); + return -1; + } + + return 1; +} + + +/* + * Create a file with the specified permissions. + */ +int +create_file(char *fname, mode_t mode) +{ + int fd; + + fd = 0; + + if (!exists_file(fname)) { + fd = open(fname, O_CREAT | O_WRONLY | O_TRUNC, mode); + if (fd == -1) { + error("could not create file %s; %s\n", fname, + strerror(errno)); + return -1; + } + close(fd); + } + + return 0; +} + + +/* + * Get the system's maximum number of bytes in a pathname. + */ +int +get_pathmax(void) +{ + int n; + + errno = 0; + + n = pathconf("/", _PC_PATH_MAX); + + if (n == -1 && errno != 0) { + error("getting PATH_MAX limit; %s\n", strerror(errno)); + return -1; + } + env.pathmax = n; + + return 0; +} diff --git a/imap.c b/imap.c new file mode 100644 index 0000000..0de81de --- /dev/null +++ b/imap.c @@ -0,0 +1,484 @@ +#include +#include +#include +#include +#include + +#include "imapfilter.h" +#include "session.h" +#include "buffer.h" + + +extern options opts; + +buffer obuf; /* Output buffer. */ + +static int tag = 0x1000; /* Every IMAP command is prefixed with a + * unique [:alnum:] string. */ + + +int send_command(session *ssn, char *cmd, char *alt); +void prepare_command(const char *fmt,...); + + +/* + * Sends to server data; a command. + */ +int +send_command(session *ssn, char *cmd, char *alt) +{ + int t = tag; + + if (ssn->socket == -1) + return -1; + + debug("sending command (%d):\n\n%s\n", ssn->socket, + (opts.debug == 1 && alt ? alt : cmd)); + + verbose("C (%d): %s", ssn->socket, (alt ? alt : cmd)); + + if (socket_write(ssn, cmd, strlen(cmd)) == -1) + return -1; + + if (tag == 0xFFFF) /* Tag always between 0x1000 and 0xFFFF. */ + tag = 0x0FFF; + tag++; + + return t; +} + + +/* + * Prepares data for sending and check that the output buffer size is + * sufficient. + */ +void +prepare_command(const char *fmt,...) +{ + int n; + va_list args; + + va_start(args, fmt); + + buffer_reset(&obuf); + n = vsnprintf(obuf.data, obuf.size + 1, fmt, args); + if (n > (int)obuf.size) { + buffer_check(&obuf, n); + vsnprintf(obuf.data, obuf.size + 1, fmt, args); + } + va_end(args); +} + + +/* + * Sends a response to a command continuation request. + */ +int +imap_continuation(session *ssn, const char *cont, size_t len) +{ + + if (ssn->socket == -1) + return -1; + + + if (socket_write(ssn, cont, len) == -1 || + socket_write(ssn, "\r\n", strlen("\r\n")) == -1) + return -1; + + if (opts.debug > 0) { + unsigned int i; + + debug("sending continuation data (%d):\n\n", ssn->socket); + + for (i = 0; i < len; i++) + debugc(cont[i]); + + debug("\r\n\n"); + + } + + return 0; +} + + +/* + * IMAP CAPABILITY: requests listing of capabilities that the server supports. + */ +int +imap_capability(session *ssn) +{ + + prepare_command("%04X CAPABILITY\r\n", tag); + + return send_command(ssn, obuf.data, NULL); +} + + +/* + * IMAP NOOP: does nothing always succeeds. + */ +int +imap_noop(session *ssn) +{ + + prepare_command("%04X NOOP\r\n", tag); + + return send_command(ssn, obuf.data, NULL); +} + + +/* + * IMAP LOGOUT: informs server that client is done. + */ +int +imap_logout(session *ssn) +{ + + prepare_command("%04X LOGOUT\r\n", tag); + + return send_command(ssn, obuf.data, NULL); +} + + +/* + * IMAP STARTTLS: begins TLS negotiation. + */ +int +imap_starttls(session *ssn) +{ + + prepare_command("%04X STARTTLS\r\n", tag); + + return send_command(ssn, obuf.data, NULL); +} + + +#ifndef NO_CRAMMD5 +/* + * IMAP AUTHENTICATE: indicates authentication mechanism and performs an + * authentication protocol exchange. + */ +int +imap_authenticate(session *ssn, const char *auth) +{ + + prepare_command("%04X AUTHENTICATE %s\r\n", tag, auth); + + return send_command(ssn, obuf.data, NULL); +} +#endif + + +/* + * IMAP LOGIN: identifies client to server. + */ +int +imap_login(session *ssn, const char *user, const char *pass) +{ + int n, r; + char c; + char *s; + + /* Command to send to server. */ + prepare_command("%04X LOGIN \"%s\" \"%s\"\r\n", tag, user, pass); + + /* Alternate command with password shrouded for safe printing. */ + n = snprintf(&c, 1, "%04X LOGIN \"%s\" *\r\n", tag, user); + s = (char *)xmalloc((n + 1) * sizeof(char)); + snprintf(s, n + 1, "%04X LOGIN \"%s\" *\r\n", tag, user); + + r = send_command(ssn, obuf.data, s); + + xfree(s); + + return r; +} + + +/* + * IMAP SELECT: accesses a mailbox in READ-WRITE mode. + */ +int +imap_select(session *ssn, const char *mbox) +{ + + prepare_command("%04X SELECT \"%s\"\r\n", tag, mbox); + + return send_command(ssn, obuf.data, NULL); +} + + +/* + * IMAP EXAMINE: accesses a mailbox in READ-ONLY mode. + */ +int +imap_examine(session *ssn, const char *mbox) +{ + + prepare_command("%04X EXAMINE \"%s\"\r\n", tag, mbox); + + return send_command(ssn, obuf.data, NULL); +} + + +/* + * IMAP CREATE: creates mailbox. + */ +int +imap_create(session *ssn, const char *mbox) +{ + + prepare_command("%04X CREATE \"%s\"\r\n", tag, mbox); + + return send_command(ssn, obuf.data, NULL); +} + + +/* + * IMAP DELETE: deletes mailbox. + */ +int +imap_delete(session *ssn, const char *mbox) +{ + + prepare_command("%04X DELETE \"%s\"\r\n", tag, mbox); + + return send_command(ssn, obuf.data, NULL); +} + + +/* + * IMAP RENAME: renames mailbox. + */ +int +imap_rename(session *ssn, const char *oldmbox, const char *newmbox) +{ + + prepare_command("%04X RENAME \"%s\" \"%s\"\r\n", tag, oldmbox, newmbox); + + return send_command(ssn, obuf.data, NULL); +} + + +/* + * IMAP SUBSCRIBE: adds the specified mailbox name to the server's set of + * "active" or "subscribed" mailboxes. + */ +int +imap_subscribe(session *ssn, const char *mbox) +{ + + prepare_command("%04X SUBSCRIBE \"%s\"\r\n", tag, mbox); + + return send_command(ssn, obuf.data, NULL); +} + + +/* + * IMAP UNSUBSCRIBE: removes the specified mailbox name to the server's set of + * "active" or "subscribed" mailboxes. + */ +int +imap_unsubscribe(session *ssn, const char *mbox) +{ + + prepare_command("%04X UNSUBSCRIBE \"%s\"\r\n", tag, mbox); + + return send_command(ssn, obuf.data, NULL); +} + + + +/* + * IMAP LIST: returns a subset of names from the complete set of all names + * available. + */ +int +imap_list(session *ssn, const char *refer, const char *name) +{ + + prepare_command("%04X LIST \"%s\" \"%s\"\r\n", tag, refer, name); + + return send_command(ssn, obuf.data, NULL); +} + + +/* + * IMAP LSUB: returns a subset of names from the set of names that the user has + * declared as being "active" or "subscribed". + */ +int +imap_lsub(session *ssn, const char *refer, const char *name) +{ + + prepare_command("%04X LSUB \"%s\" \"%s\"\r\n", tag, refer, name); + + return send_command(ssn, obuf.data, NULL); +} + + +/* + * IMAP STATUS: requests status of the indicated mailbox. + */ +int +imap_status(session *ssn, const char *mbox, const char *items) +{ + + prepare_command("%04X STATUS \"%s\" (%s)\r\n", tag, mbox, items); + + return send_command(ssn, obuf.data, NULL); +} + + +/* + * IMAP APPEND: append message to the end of a mailbox. + */ +int +imap_append(session *ssn, const char *mbox, const char *flags, + const char *date, unsigned int size) +{ + + prepare_command("%04X APPEND \"%s\"%s%s%s%s%s%s {%d}\r\n", tag, mbox, + (flags ? " (" : ""), (flags ? flags : ""), (flags ? ")" : ""), + (date ? " \"" : ""), (date ? date : ""), (date ? "\"" : ""), size); + + return send_command(ssn, obuf.data, NULL); +} + + +/* + * IMAP CHECK: requests a checkpoint of the currently selected mailbox. + */ +int +imap_check(session *ssn) +{ + + prepare_command("%04X CHECK\r\n", tag); + + return send_command(ssn, obuf.data, NULL); +} + + +/* + * IMAP CLOSE: deletes messages and returns to authenticated state. + */ +int +imap_close(session *ssn) +{ + + prepare_command("%04X CLOSE\r\n", tag); + + return send_command(ssn, obuf.data, NULL); +} + + +/* + * IMAP EXPUNGE: permanently removes any messages with the \Deleted flag set. + */ +int +imap_expunge(session *ssn) +{ + + prepare_command("%04X EXPUNGE\r\n", tag); + + return send_command(ssn, obuf.data, NULL); +} + + +/* + * IMAP SEARCH: searches the mailbox for messages that match certain criteria. + */ +int +imap_search(session *ssn, const char *charset, const char *criteria) +{ + + if (charset != NULL && *charset != '\0') + prepare_command("%04X UID SEARCH CHARSET \"%s\" %s\r\n", tag, + charset, criteria); + else + prepare_command("%04X UID SEARCH %s\r\n", tag, + criteria); + + return send_command(ssn, obuf.data, NULL); +} + + +/* + * IMAP FETCH: retrieves data associated with a message. + */ +int +imap_fetch(session *ssn, const char *mesg, const char *items) +{ + + prepare_command("%04X UID FETCH %s %s\r\n", tag, mesg, items); + + return send_command(ssn, obuf.data, NULL); +} + + +/* + * IMAP STORE: alters data associated with a message. + */ +int +imap_store(session *ssn, const char *mesg, const char *mode, + const char *flags) +{ + + prepare_command("%04X UID STORE %s %sFLAGS.SILENT (%s)\r\n", tag, + mesg, (!strncasecmp(mode, "add", 3) ? "+" : + !strncasecmp(mode, "remove", 6) ? "-" : ""), flags); + + return send_command(ssn, obuf.data, NULL); +} + + +/* + * IMAP COPY: copy messages to mailbox. + */ +int +imap_copy(session *ssn, const char *mesg, const char *mbox) +{ + + prepare_command("%04X UID COPY %s \"%s\"\r\n", tag, mesg, mbox); + + return send_command(ssn, obuf.data, NULL); +} + + +/* + * IMAP NAMESPACE: discovers the prefix and delimeter of namespaces used by the + * server for mailboxes (RFC 2342). + */ +int +imap_namespace(session *ssn) +{ + + prepare_command("%04X NAMESPACE\r\n", tag); + + return send_command(ssn, obuf.data, NULL); +} + + +/* + * IMAP IDLE: enter the idle state and waits for mailbox updates, as specified + * in the IMAP IDLE extension (RFC 2177). + */ +int +imap_idle(session *ssn) +{ + prepare_command("%04X IDLE\r\n", tag); + + return send_command(ssn, obuf.data, NULL); +} + + +/* + * IMAP DONE: ends the idle state entered through the IMAP IDLE command + * (RFC 2177). + */ +int +imap_done(session *ssn) +{ + + return imap_continuation(ssn, "DONE", strlen("DONE")); +} diff --git a/imapfilter.1 b/imapfilter.1 new file mode 100644 index 0000000..d064a48 --- /dev/null +++ b/imapfilter.1 @@ -0,0 +1,74 @@ +.Dd February 28, 2011 +.Dt IMAPFILTER 1 +.Os +.Sh NAME +.Nm imapfilter +.Nd mail filter +.Sh SYNOPSIS +.Nm +.Op Fl Vdiv +.Op Fl c Ar configfile +.Op Fl e Ar 'command' +.Op Fl l Ar logfile +.Sh DESCRIPTION +.Nm +is a mail filtering utility. It connects to remote mail servers using the +Internet Message Access Protocol (IMAP), sends searching queries to the server +and processes mailboxes based on the results. It can be used to delete, copy, +move, flag, etc. messages residing in mailboxes at the same or different mail +servers. The 4rev1 and 4 versions of the IMAP protocol are supported. +.Pp +The command line options of +.Xr imapfilter 1 +are as follows: +.Bl -tag -width Ds +.It Fl V +Displays program's version. +.It Fl c Ar configfile +Path to the configuration file. The default is +.Pa $HOME/.imapfilter/config.lua . +.It Fl d +Debug mode; creates a temporary file name in +.Pa $HOME/.imapfilter/debug.XXXXXX , +where debugging messages about the program's progress are printed. +.It Fl e Ar 'command' +May be used to enter one line of program (configuration). +When this options is used, +.Nm +will not look for any configuration file. +.It Fl i +Enters interactive mode after executing the configuration file or the command +line. +.It Fl l Ar logfile +File that contains logs of error messages +.Nm +produces. +.It Fl v +Verbose mode; prints detailed information about the program's actions. +.El +.Sh ENVIRONMENT +.Bl -tag -width Ds +.It Ev HOME +User's home directory. +.El +.Sh FILES +.Bl -tag -width Ds +.It Pa $HOME/.imapfilter/config.lua +Default configuration file. Because this file may contain sensitive data such +as user passwords, the recommended permissions are read/write for the user, and +not accessible by others. +.It Pa $HOME/.imapfilter/certificates +File where the SSL/TLS certificates are stored. +.It Pa $HOME/.imapfilter/debug.XXXXXX +Debug files. +.El +.Sh SEE ALSO +.Xr imapfilter_config 5 +.Sh CONFORMING TO +.Bl -tag -width Ds +.It IMAP4rev1: +RFC 3501, RFC 3348, RFC 2683, RFC 2595, RFC 2342, RFC 2195, +RFC 2177 +.It IMAP4: +RFC 1730 +.El diff --git a/imapfilter.c b/imapfilter.c new file mode 100644 index 0000000..f9213c7 --- /dev/null +++ b/imapfilter.c @@ -0,0 +1,186 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "imapfilter.h" +#include "session.h" +#include "list.h" +#include "version.h" +#include "buffer.h" +#include "pathnames.h" +#include "regexp.h" + +#ifndef NO_SSLTLS +#include +#include +#endif + + +extern buffer ibuf, obuf, nbuf; +extern regexp responses[]; + +options opts; /* Program options. */ +environment env; /* Environment variables. */ + +list *sessions = NULL; /* Active IMAP sessions. */ + +void usage(void); +void version(void); + + +/* + * IMAPFilter: an IMAP mail filtering utility. + */ +int +main(int argc, char *argv[]) +{ + int c; + + setlocale(LC_CTYPE, ""); + + opts.debug = 0; + opts.verbose = 0; + opts.interactive = 0; + opts.log = NULL; + opts.config = NULL; + opts.oneline = NULL; + + env.home = getenv("HOME"); + env.pathmax = -1; + + while ((c = getopt(argc, argv, "Vc:de:il:v?")) != -1) { + switch (c) { + case 'V': + version(); + /* NOTREACHED */ + break; + case 'c': + opts.config = optarg; + break; + case 'd': + if (opts.debug < 2) + opts.debug++; + break; + case 'e': + opts.oneline = optarg; + break; + case 'i': + opts.interactive = 1; + break; + case 'l': + opts.log = optarg; + break; + case 'v': + opts.verbose = 1; + break; + case '?': + default: + usage(); + /* NOTREACHED */ + break; + } + } + + get_pathmax(); + + open_debug(); + + create_homedir(); + + catch_signals(); + + open_log(); + + buffer_init(&ibuf, INPUT_BUF); + buffer_init(&obuf, OUTPUT_BUF); + buffer_init(&nbuf, NAMESPACE_BUF); + + if (opts.config == NULL) { + int n; + char b; + + n = snprintf(&b, 1, "%s/%s", env.home, PATHNAME_CONFIG); + + if (env.pathmax != -1 && n > env.pathmax) + fatal(ERROR_PATHNAME, + "pathname limit %ld exceeded: %d\n", env.pathmax, + n); + + opts.config = (char *)xmalloc((n + 1) * sizeof(char)); + snprintf(opts.config, n + 1, "%s/%s", env.home, + PATHNAME_CONFIG); + } + + regexp_compile(responses); + +#ifndef NO_SSLTLS + SSL_library_init(); + SSL_load_error_strings(); +#endif + + start_lua(); + + { + list *l; + session *s; + + l = sessions; + while (l != NULL) { + s = l->data; + l = l->next; + + response_generic(s, imap_logout(s)); + close_connection(s); + session_destroy(s); + } + } + + stop_lua(); + +#ifndef NO_SSLTLS + ERR_free_strings(); +#endif + + regexp_free(responses); + + buffer_free(&ibuf); + buffer_free(&obuf); + buffer_free(&nbuf); + + close_log(); + close_debug(); + + exit(0); +} + + +/* + * Print a very brief usage message. + */ +void +usage(void) +{ + + fprintf(stderr, "usage: imapfilter [-Vdiv] [-c configfile] " + "[-e 'command'] [-l logfile]\n"); + + exit(0); +} + + +/* + * Print program's version and copyright. + */ +void +version(void) +{ + + fprintf(stderr, "IMAPFilter %s %s\n", VERSION, COPYRIGHT); + + exit(0); +} diff --git a/imapfilter.h b/imapfilter.h new file mode 100644 index 0000000..d6cbe64 --- /dev/null +++ b/imapfilter.h @@ -0,0 +1,285 @@ +#ifndef IMAPFILTER_H +#define IMAPFILTER_H + + +#include +#include +#include + +#include +#include + +#include "session.h" + +#ifndef NO_SSLTLS +#include +#endif + + +/* Fatal error exit codes. */ +#define ERROR_SIGNAL 1 +#define ERROR_CONFIG 2 +#define ERROR_MEMALLOC 3 +#define ERROR_PATHNAME 4 +#define ERROR_CERTIFICATE 5 + +/* IMAP protocol supported by the server. */ +#define PROTOCOL_NONE 0 +#define PROTOCOL_IMAP4REV1 1 +#define PROTOCOL_IMAP4 2 + +/* Capabilities of mail server. */ +#define CAPABILITY_NONE 0x00 +#define CAPABILITY_NAMESPACE 0x01 +#define CAPABILITY_CRAMMD5 0x02 +#define CAPABILITY_STARTTLS 0x04 +#define CAPABILITY_CHILDREN 0x08 +#define CAPABILITY_IDLE 0x10 + +/* Status responses and response codes. */ +#define STATUS_RESPONSE_NONE 0 +#define STATUS_RESPONSE_OK 1 +#define STATUS_RESPONSE_NO 2 +#define STATUS_RESPONSE_BAD 3 +#define STATUS_RESPONSE_UNTAGGED 4 +#define STATUS_RESPONSE_CONTINUE 5 +#define STATUS_RESPONSE_BYE 6 +#define STATUS_RESPONSE_PREAUTH 7 +#define STATUS_RESPONSE_READONLY 8 +#define STATUS_RESPONSE_TRYCREATE 9 +#define STATUS_RESPONSE_TIMEOUT 10 + + +/* Initial buffer size for input, output and namespace buffers. */ +#define INPUT_BUF 4096 +#define OUTPUT_BUF 1024 +#define NAMESPACE_BUF 128 + +/* Maximum length, in bytes, of a utility's input line. */ +#ifndef LINE_MAX +#define LINE_MAX 2048 +#endif + + +/* Program's options. */ +typedef struct options { + int debug; /* Debugging level (0..2). */ + int verbose; /* Verbose mode. */ + int interactive; /* Act as an interpreter. */ + char *log; /* Log file for error messages. */ + char *config; /* Configuration file. */ + char *oneline; /* One line of program/configuration. */ +} options; + +/* Environment variables. */ +typedef struct environment { + char *home; /* User's home directory. */ + long pathmax; /* Maximum pathname. */ +} environment; + + +/* auth.c */ +#ifndef NO_CRAMMD5 +int auth_cram_md5(session *ssn, const char *user, const char *pass); +#endif + +/* cert.c */ +#ifndef NO_SSLTLS +int get_cert(session *ssn); +#endif + +/* core.c */ +LUALIB_API int luaopen_ifcore(lua_State *lua); + +/* file.c */ +int create_homedir(void); +int exists_file(char *fname); +int exists_dir(char *fname); +int create_file(char *fname, mode_t mode); +int get_pathmax(void); + +/* imap.c */ +int imap_continuation(session *ssn, const char *cont, size_t len); +int imap_capability(session *ssn); +int imap_noop(session *ssn); +int imap_logout(session *ssn); +#ifndef NO_SSLTLS +int imap_starttls(session *ssn); +#endif +int imap_authenticate(session *ssn, const char *auth); +int imap_login(session *ssn, const char *user, const char *pass); +int imap_select(session *ssn, const char *mbox); +int imap_examine(session *ssn, const char *mbox); +int imap_create(session *ssn, const char *mbox); +int imap_delete(session *ssn, const char *mbox); +int imap_rename(session *ssn, const char *oldmbox, const char *newmbox); +int imap_subscribe(session *ssn, const char *mbox); +int imap_unsubscribe(session *ssn, const char *mbox); +int imap_list(session *ssn, const char *refer, const char *name); +int imap_lsub(session *ssn, const char *refer, const char *name); +int imap_status(session *ssn, const char *mbox, const char *items); +int imap_append(session *ssn, const char *mbox, const char *flags, + const char *date, unsigned int size); +int imap_check(session *ssn); +int imap_close(session *ssn); +int imap_expunge(session *ssn); +int imap_search(session *ssn, const char *charset, const char *criteria); +int imap_fetch(session *ssn, const char *mesg, const char *items); +int imap_store(session *ssn, const char *mesg, const char *mode, + const char *flags); +int imap_copy(session *ssn, const char *mesg, const char *mbox); +int imap_namespace(session *ssn); +int imap_idle(session *ssn); +int imap_done(session *ssn); + +/* log.c */ +void verbose(const char *info,...); +void debug(const char *debug,...); +void debugc(char c); +void error(const char *errmsg,...); +void fatal(unsigned int errnum, const char *fatal,...); + +int open_debug(void); +int close_debug(void); + +int open_log(void); +int close_log(void); + +/* lua.c */ +void start_lua(void); +void stop_lua(void); + +int get_option_boolean(const char *opt); +lua_Number get_option_number(const char *opt); +const char *get_option_string(const char *opt); + +int get_table_type(const char *key); +lua_Number get_table_number(const char *key); +const char *get_table_string(const char *key); + +int set_table_nil(const char *key); +int set_table_boolean(const char *key, int value); +int set_table_number(const char *key, lua_Number value); +int set_table_string(const char *key, const char *value); + +/* memory.c */ +void *xmalloc(size_t size); +void *xrealloc(void *ptr, size_t size); +void xfree(void *ptr); +char *xstrdup(const char *str); +char *xstrndup(const char *str, size_t len); + +/* misc.c */ +const char *xstrcasestr(const char *haystack, const char *needle); +char *xstrncpy(char *dest, const char *src, size_t size); + +/* namespace.c */ +const char *apply_namespace(const char *mbox, char *prefix, char delim); +const char *reverse_namespace(const char *mbox, char *prefix, char delim); + +/* pcre.c */ +LUALIB_API int luaopen_ifre(lua_State *lua); + +/* request.c */ +int request_noop(const char *server, const char *port, const char *user); +int request_login(const char *server, const char *port, const char *ssl, + const char *user, const char *pass); +int request_logout(const char *server, const char *port, const char *user); +int request_status(const char *server, const char *port, const char *user, + const char *mbox, unsigned int *exist, unsigned int *recent, + unsigned int *unseen, unsigned int *uidnext); +int request_select(const char *server, const char *port, const char *user, + const char *mbox); +int request_close(const char *server, const char *port, const char *user); +int request_expunge(const char *server, const char *port, const char *user); +int request_list(const char *server, const char *port, const char *user, + const char *refer, const char *name, char **mboxs, char **folders); +int request_lsub(const char *server, const char *port, const char *user, + const char *refer, const char *name, char **mboxs, char **folders); +int request_search(const char *server, const char *port, const char *user, + const char *criteria, const char *charset, char **mesgs); +int request_fetchfast(const char *server, const char *port, const char *user, + const char *mesg, char **flags, char **date, char **size); +int request_fetchflags(const char *server, const char *port, const char *user, + const char *mesg, char **flags); +int request_fetchdate(const char *server, const char *port, const char *user, + const char *mesg, char **date); +int request_fetchsize(const char *server, const char *port, const char *user, + const char *mesg, char **size); +int request_fetchstructure(const char *server, const char *port, + const char *user, const char *mesg, char **structure); +int request_fetchheader(const char *server, const char *port, const char *user, + const char *mesg, char **header, size_t *len); +int request_fetchtext(const char *server, const char *port, const char *user, + const char *mesg, char **text, size_t *len); +int request_fetchfields(const char *server, const char *port, const char *user, + const char *mesg, const char *headerfields, char **fields, size_t *len); +int request_fetchpart(const char *server, const char *port, const char *user, + const char *mesg, const char *bodypart, char **part, size_t *len); +int request_store(const char *server, const char *port, const char *user, + const char *mesg, const char *mode, const char *flags); +int request_copy(const char *server, const char *port, const char *user, + const char *mesg, const char *mbox); +int request_append(const char *server, const char *port, const char *user, + const char *mbox, const char *mesg, size_t mesglen, const char *flags, + const char *date); +int request_create(const char *server, const char *port, const char *user, + const char *mbox); +int request_delete(const char *server, const char *port, const char *user, + const char *mbox); +int request_rename(const char *server, const char *port, const char *user, + const char *oldmbox, const char *newmbox); +int request_subscribe(const char *server, const char *port, const char *user, + const char *mbox); +int request_unsubscribe(const char *server, const char *port, const char *user, + const char *mbox); +int request_idle(const char *server, const char *port, const char *user); + + +/* response.c */ +int response_generic(session *ssn, int tag); +int response_continuation(session *ssn); +int response_greeting(session *ssn); +int response_capability(session *ssn, int tag); +int response_authenticate(session *ssn, int tag, unsigned char **cont); +int response_namespace(session *ssn, int tag); +int response_status(session *ssn, int tag, unsigned int *exist, + unsigned int *recent, unsigned int *unseen, unsigned int *uidnext); +int response_examine(session *ssn, int tag, unsigned int *exist, + unsigned int *recent); +int response_select(session *ssn, int tag); +int response_list(session *ssn, int tag, char **mboxs, char **folders); +int response_search(session *ssn, int tag, char **mesgs); +int response_fetchfast(session *ssn, int tag, char **flags, char **date, + char **size); +int response_fetchflags(session *ssn, int tag, char **flags); +int response_fetchdate(session *ssn, int tag, char **date); +int response_fetchsize(session *ssn, int tag, char **size); +int response_fetchstructure(session *ssn, int tag, char **structure); +int response_fetchbody(session *ssn, int tag, char **body, size_t *len); +int response_idle(session *ssn, int tag); + +/* signal.c */ +void catch_signals(void); +void release_signals(void); + +/* socket.c */ +int open_connection(session *ssn, const char *server, const char *port, + const char *protocol); +int close_connection(session *ssn); +ssize_t socket_read(session *ssn, char *buf, size_t len, long timeout, + int timeoutfail); +ssize_t socket_write(session *ssn, const char *buf, size_t len); +#ifndef NO_SSLTLS +int open_secure_connection(session *ssn, const char *server, const char *port, + const char *protocol); +int close_secure_connection(session *ssn); +ssize_t socket_secure_read(session *ssn, char *buf, size_t len); +ssize_t socket_secure_write(session *ssn, const char *buf, size_t len); +#endif + +/* system.c */ +LUALIB_API int luaopen_ifsys(lua_State *lua); + + +#endif /* IMAPFILTER_H */ diff --git a/imapfilter_config.5 b/imapfilter_config.5 new file mode 100644 index 0000000..5a2a1f7 --- /dev/null +++ b/imapfilter_config.5 @@ -0,0 +1,1112 @@ +.Dd February 28, 2011 +.Dt IMAPFILTER_CONFIG 5 +.Os +.Sh NAME +.Nm imapfilter_config +.Nd imapfilter configuration file +.Sh SYNOPSIS +.Pa $HOME/.imapfilter/config.lua +.Sh DESCRIPTION +.Xr imapfilter 1 +uses the Lua programming language as a configuration and extension language, +therefore the configuration file is a Lua script. +.Pp +Although knowledge of Lua is not required to use +.Xr imapfilter 1 , +it is nonetheless recommended, especially if one wants to extend it. +.Sh CONVENTIONS +.Pp +A brief description of the Lua values and types mentioned hereafter in the +manual page follows: +.Bl -item -offset 4n +.It +The +.Vt nil +is the type of the value ``nil'' , +whose main property is to be different from any other value; usually it +represents the absence of a useful value. +.It +The +.Vt boolean +is the type of the values ``true'' and ``false''. Both ``nil'' and ``false'' +make a condition false; any other value makes it true. +.It +The type +.Vt number +represents real numbers. +.It +The type +.Vt string +represents a sequence of characters and can be defined using single quotes, +double quotes or double square brackets. +.It +The type +.Vt table +implements associative arrays, that is, arrays that can be indexed not only +with numbers, but with any value. +.It +A +.Vt function +is a first-class value; it can be stored in variables, passed as argument to +other functions, and returned as a result. +.El +.Sh OPTIONS +Program's options are set using an already initialised +.Vt table +named ``options'' , +in the following manner: +.Bd -literal -offset 4n +options.timeout = 120 +options.namespace = false +options.charset = 'ISO-8859-1' +.Ed +.Pp +Available options are: +.Bl -tag -width Ds +.It Va cache +When this option is enabled, parts of messages are cached locally in memory to +avoid being downloaded more than once. The cache is preserved for the current +session only. This variable takes a +.Vt boolean +as a value. Default is ``true''. +.It Va certificates +When this option is enabled, the server certificate can be accepted and stored, +in order to validate the authenticity of the server in future connections. This +variable takes a +.Vt boolean +as a value. Default is ``true''. +.It Va charset +Indicates to the server the character set of the strings for the searching +methods. This variable takes a +.Vt string +as a value. By default no character set is set, and thus plain ASCII should be +assumed by the server. +.It Va create +According to the IMAP specification, when trying to write a message to a +non-existent mailbox, the server must send a hint to the client, whether it +should create the mailbox and try again or not. However some IMAP servers don't +follow the specification and don't send the correct response code to the +client. By enabling this option the client tries to create the mailbox, despite +of the server's response. This variable takes a +.Vt boolean +as a value. Default is ``false''. +.It Va close +This option controls whether the currently selected mailbox is implicitly +closed at the end of each performed operation, thus removing all messages that +are marked deleted. This variable takes a +.Vt boolean +as a value. Default is ``false''. +.It Va crammd5 +When this option is enabled and the server supports the Challenge-Response +Authentication Mechanism (specifically CRAM-MD5), this method will be used for +user authentication instead of a plaintext password LOGIN. This variable +takes a +.Vt boolean +as a value. Default is ``true''. +.It Va expunge +Normally, messages are marked for deletion and are actually deleted when the +mailbox is closed. When this option is enabled, messages are expunged +immediately after being marked deleted. This variable takes a +.Vt boolean +as a value. Default is ``true''. +.It Va info +When this options is enabled, a summary of the program's actions is printed, +while processing mailboxes. This variable takes a +.Vt boolean +as a value. Default is ``true''. +.It Va keepalive +The time in minutes before terminating and re-issuing the IDLE command, in +order to keep alive the connection, by resetting the inactivity timeout of the +server. A standards compliant server must have an inactivity timeout of at +least 30 minutes. But it may happen that some IMAP servers don't respect that, +or some intermediary network device has a shorter timeout. By setting this +option the above problem can be worked around. This variable takes a +.Vt number +as a value. Default is ``29'' minutes. +.It Va namespace +When enabled, the program gets the namespace of the user's personal mailboxes, +and applies automatically the prefix and hierarchy delimiter to any mailboxes +residing on the mail server; the user must use the `/' character as the +delimiter and ``'' (ie. nothing) as the prefix, regardless of the folder +format of the mail server. This must be disabled, if the user wants to +manually specify mailbox names (eg. because they are not part of the user's +personal namespace mailboxes). This variable takes +.Vt boolean +as a value. Default is ``true''. +.It Va starttls +When this option is enabled and the server supports the IMAP STARTTLS +extension, a TLS connection will be negotiated with the mail server in the +beginning of the session. This variable takes a +.Vt boolean +as value. Default is ``true''. +.It Va subscribe +By enabling this option new mailboxes that were automatically created, get also +subscribed; they are set active in order for IMAP clients to recognize them. +This variable takes a +.Vt boolean +as a value. Default is ``false''. +.It Va timeout +The time in seconds for the program to wait for a mail server's response. If +not set, the client will block indefinitely. This variable takes a +.Vt number +as a value. By default no value is set. +.El +.Sh ACCOUNTS +Accounts are initialized using the +.Fn IMAP +function, and the details of the connection are defined using an account +.Vt table : +.Bd -literal -offset 4n +myaccount = IMAP { + server = 'imap.mail.server', + username = 'me', + password = 'secret', + ssl = 'ssl3' +} +.Ed +.Pp +An account +.Vt table +must have the following elements: +.Bl -tag -width Ds +.It Va server +The hostname of the IMAP server to connect to. It takes a +.Vt string +as a value. +.It Va username +User's name. It takes a +.Vt string +as a value. +.El +.Pp +An account +.Vt table +can also have the following optional elements: +.Bl -tag -width Ds +.It Va password +User's secret keyword. If a password wasn't supplied the user will be asked to +enter one interactively the first time it will be needed. It takes a +.Vt string +as a value. +.It Va port +The port to connect to. It takes a +.Vt number +as a value. Default is ``143'' for imap and ``993'' for imaps. +.It Va ssl +Forces an imaps connection and specifies the SSL/TLS protocol to be used. It +takes a +.Vt string +as a value, specifically one of: ``ssl2'' , ``ssl3'' , ``tls1''. +.El +.Pp +.Ss LISTING +The following methods can be used on an account to list mailboxes in a folder +of an account: +.Pp +.Bl -tag -width Ds -compact +.It Fn list_all folder +Lists all the available mailboxes in the +.Fa folder +.Pq Vt string , +and returns a +.Vt table +that contains +.Vt strings , +the available mailboxes, +and a +.Vt table +that contains +.Vt strings , +the available folders. +.Pp +.It Fn list_subscribed folder +Lists all the subscribed mailboxes in the +.Fa folder +.Pq Vt string , +and returns a +.Vt table +that contains +.Vt strings , +the subscribed mailboxes, +and a +.Vt table +that contains +.Vt strings , +the subscribed folders. +.El +.Pp +The following methods can be used on an account to list mailboxes, using +wildcards, in a folder of an account. The `*' wildcard, matches any character +and the `%' matches any character except the folder delimiter, ie. +non-recursively: +.Pp +.Bl -tag -width Ds -compact +.It Fn list_all folder mailbox +Lists all the available mailboxes in the +.Fa folder +.Pq Vt string +with the name +.Fa mailbox +.Pq Vt string , +and returns a +.Vt table +that contains +.Vt strings , +the available mailboxes, +and a +.Vt table +that contains +.Vt strings , +the available folders. Wildcards may only be used in the +.Fa mailbox +argument. +.Pp +.It Fn list_subscribed folder mailbox +Lists all the subscribed mailboxes in the +.Fa folder +.Pq Vt string +with the name +.Fa mailbox +.Pq Vt string , +and returns a +.Vt table +that contains +.Vt strings , +the subscribed mailboxes, +and a +.Vt table +that contains +.Vt strings , +the subscribed folders. Wildcards may only be used in the +.Fa mailbox +argument. +.El +.Pp +Examples: +.Bd -literal -offset 4n +mailboxes, folders = myaccount:list_subscribed('myfolder') +mailboxes, folders = myaccount:list_all('myfolder/mysubfolder', '*') +.Ed +.Ss MANIPULATING +The following methods can be used to manipulate mailboxes in an account: +.Pp +.Bl -tag -width Ds -compact +.It Fn create_mailbox name +Creates the +.Fa name +.Pq Vt string +mailbox. +.Pp +.It Fn delete_mailbox name +Deletes the +.Fa name +.Pq Vt string +mailbox. +.Pp +.It Fn rename_mailbox oldname newname +Renames the +.Fa oldname +.Pq Vt string +mailbox to +.Fa newname +.Pq Vt string . +.Pp +.It Fn subscribe_mailbox name +Subscribes the +.Fa name +.Pq Vt string +mailbox. +.Pp +.It Fn unsubscribe_mailbox name +Unsubscribes the +.Fa name +.Pq Vt string +mailbox. +.El +.Pp +Examples: +.Bd -literal -offset 4n +myaccount:create_mailbox('mymailbox') +myaccount:subscribe_mailbox('mymailbox') +myaccount:unsubscribe_mailbox('myfolder/mymailbox') +myaccount:delete_mailbox('myfolder/mymailbox') +.Ed +.Sh MAILBOXES +After an IMAP account has been initialized, mailboxes residing in that account +can be accessed simply as elements of the account +.Vt table : +.Bd -literal -offset 4n +myaccount.mymailbox +.Ed +.Pp +If mailbox names don't only include letters, digits and underscores, or begin +with a digit, an alternative form must be used: +.Bd -literal -offset 4n +myaccount['mymailbox'] +.Ed +.Pp +A mailbox inside a folder can be only accessed by using the alternative form: +.Bd -literal -offset 4n +myaccount['myfolder/mymailbox'] +.Ed +.Pp +The methods that are available for an account (eg. +.Fn list_all , +.Fn create_mailbox , +etc.) , are considered keywords and must not be used as mailbox names, and the +same also applies for any string starting with an underscore, as they are +considered reserved. +.Ss CHECKING +The following methods can be used to check the status of a mailbox: +.Pp +.Bl -tag -width Ds -compact +.It Fn check_status +.Pp +The +.Fn check_status +method gets the current status of a mailbox, and returns four values of +.Vt number +type: the total number of messages, the number of recent messages, the +number of unseen messages in the mailbox, and the next UID to be assigned to a +new message in the mailbox. +.Pp +.It Fn enter_idle +The +.Fn enter_idle +method implements the IMAP IDLE (RFC 2177) extension. By using this extension +it's not necessary to poll the server for changes to the selected mailbox (ie. +using the +.Fn check_status +method), but instead the server sends an update when there is a change +in the mailbox (eg. in case of new mail). When the +.Fn enter_idle +method has been called no more commands in the configuration file are executed +until an update is received, at which point the +.Fn enter_idle +method returns. For the +.Fn enter_idle +to work, the IDLE extension has to be supported by the IMAP server. The +.Fn enter_idle +method returns one value of type +.Vt boolean : +``true'' if the IDLE extension is supported and there was a update in the +mailbox, and ``false'' if the IDLE extension is not supported, in which case +the method returns immediately. +.El +.Pp +Examples: +.Bd -literal -offset 4n +exist, unread, unseen, uidnext = myaccount.mymailbox:check_status() +update = myaccount.mymailbox:enter_idle() +.Ed +.Ss SEARCHING +.Pp +The searching methods in this subsection can be applied to any mailbox. +They return a special form of +.Vt table , +that contains the messages that match the searching method. This +.Vt table +can be combined with other +.Vt tables +using logic theory. There are three available operations, that implement +logical ``or'', logical ``and'' and logical ``not''. +.Pp +The logical ``or'' is implemented using the `+' operator: +.Bd -literal -offset 4n +results = myaccount.mymailbox:is_unseen() + + myaccount.mymailbox:is_larger(100000) +.Ed +.Pp +The logical ``and'' is implemented using the `*' operator: +.Bd -literal -offset 4n +results = myaccount.mymailbox:is_unseen() * + myaccount.mymailbox:is_larger(100000) +.Ed +.Pp +The logical ``not'' is implemented using the `-' operator: +.Bd -literal -offset 4n +results = myaccount.mymailbox:is_unseen() - + myaccount.mymailbox:is_larger(100000) +.Ed +.Pp +The three logical operators can be combined in the same expression. The logical +``and'' has higher precedence than the logical ``or'' and the logical ``not'', +with the latter two having the same precedence, and parentheses may be used to +change this behaviour: +.Bd -literal -offset 4n +results = myaccount.mymailbox:is_unseen() + + myaccount.mymailbox:is_larger(100000) * + myaccount.mymailbox:contain_subject('test') + +results = ( myaccount.mymailbox:is_unseen() + + myaccount.mymailbox:is_larger(100000) ) * + myaccount.mymailbox:contain_subject('test') +.Ed +.Pp +The returned +.Vt tables +of the searching methods can also be stored in variables and then further +processed: +.Bd -literal -offset 4n +unseen = myaccount.myaccount:is_unseen() +larger = myaccount.mymailbox:is_larger(100000) +subject = myaccount.mymailbox:contain_subject('test') +results = unseen + larger * subject +.Ed +.Pp +A composite filter that includes one or more simple rules can be defined: +.Bd -literal -offset 4n +myfilter = function () + return myaccount.mymailbox:is_unseen() + + myaccount.mymailbox:is_larger(100000) * + myaccount.mymailbox:contain_subject('test') + end + +results = myfilter() +.Ed +.Pp +Composite filters can may be more dynamic by adding arguments: +.Bd -literal -offset 4n +myfilter = function (mailbox, size, subject) + return mailbox:is_unseen() + + mailbox:is_larger(size) * + mailbox:contain_subject(subject) + end + +results = myfilter(myaccount.mailbox, 100000, 'test') +.Ed +.Pp +It is also possible to combine the searching methods in different mailboxes, +either at the same or different accounts, for example when the same actions +will be executed on messages residing in different mailboxes or accounts. +.Bd -literal -offset 4n +results = myaccount.mymailbox:is_unseen() + + myaccount.othermailbox:is_larger(100000) + + otheraccount.othermailbox:contain_subject('test') +.Ed +.Pp +The following method can be used to get all messages in a mailbox: +.Pp +.Bl -tag -width Ds -compact +.It Fn select_all +All messages. +.El +.Pp +The following methods can be used to search for messages that are in a specific +state: +.Pp +.Bl -tag -width Ds -compact +.It Fn is_answered +Messages that have been answered. +.Pp +.It Fn is_deleted +Messages that are marked for later removal. +.Pp +.It Fn is_draft +Messages that have not completed composition. +.Pp +.It Fn is_flagged +Messages that are flagged for urgent/special attention. +.Pp +.It Fn is_new +Messages that are recently arrived (this session is the first to have been +notified about these messages) and have not been read. +.Pp +.It Fn is_old +Messages that are not recently arrived (this session is not the first to have +been notified about these messages) and have not been read. +.Pp +.It Fn is_recent +Messages that are recently arrived (this session is the first to have been +notified about these messages). +.Pp +.It Fn is_seen +Messages that have been read. +.Pp +.It Fn is_unanswered +Messages that have not been answered. +.Pp +.It Fn is_undeleted +Messages that are not marked for later removal. +.Pp +.It Fn is_undraft +Messages that have completed composition. +.Pp +.It Fn is_unflagged +Messages that are not flagged for urgent/special attention. +.Pp +.It Fn is_unseen +Messages that have not been read. +.El +.Pp +The following method can be used to search for messages that have a specific +flag set: +.Pp +.Bl -tag -width Ds -compact +.It Fn has_flag keyword +Messages with the specified keyword flag +.Pq Vt string +set. +.El +.Pp +The following methods can be used to search for messages based on their size: +.Pp +.Bl -tag -width Ds -compact +.It Fn is_larger size +Messages that are larger than the size +.Pq Vt number +in octets (bytes). +.Pp +.It Fn is_smaller size +Messages that are smaller than the size +.Pq Vt number +in octets (bytes). +.El +.Pp +The following methods can be used to search for messages based on their age: +.Pp +.Bl -tag -width Ds -compact +.It Fn is_newer age +Messages that are newer than the +.Fa age +.Pq Vt number +in days. +.Pp +.It Fn is_older age +Messages that are older than the +.Fa age +.Pq Vt number +in days. +.El +.Pp +The following methods can be used to search for messages based on their arrival +or sent date, in the ``day-month-year'' form, where day is the day of the month +as a decimal number (01-31), month is the abbreviated month (``Jan'', ``Feb'', +``Mar'', ``Apr'', ``May'', ``Jun'', ``Jul'', ``Aug'', ``Sep'', ``Oct'', +``Nov'', ``Dec'') and year is the year as decimal number including the century +(eg. 2007): +.Pp +.Bl -tag -width Ds -compact +.It Fn arrived_before date +messages that have arrived before the +.Fa date +.Pq Vt string , +where +.Fa date +is in the ``day-month-year'' form. +.Pp +.It Fn arrived_on date +Messages that have arrived on the +.Fa date +.Pq Vt string , +where +.Fa date +is in the ``day-month-year'' form. +.Pp +.It Fn arrived_since date +Messages that have arrived after the +.Fa date +.Pq Vt string , +where +.Fa date +is in the ``day-month-year'' form. +.Pp +.It Fn sent_before date +Messages that have been sent before the +.Fa date +.Pq Vt string , +where +.Fa date +is in the ``day-month-year'' form. +.Pp +.It Fn sent_on date +Messages that have been sent on the +.Fa date +.Pq Vt string , +where +.Fa date +is in the ``day-month-year'' form. +.Pp +.It Fn sent_since date +Messages that have been sent after the +.Fa date +.Pq Vt string , +where +.Fa date +is in the ``day-month-year'' form. +.El +.Pp +The following methods can be used to search for messages that contain a +specific word or phrase: +.Pp +.Bl -tag -width Ds -compact +.It Fn contain_bcc string +Messages that contain the +.Fa string +.Pq Vt string +in the ``Bcc'' header field. +.Pp +.It Fn contain_cc string +Messages that contain the +.Fa string +.Pq Vt string +in the ``Cc'' header field. +.Pp +.It Fn contain_from string +Messages that contain the +.Fa string +.Pq Vt string +in the ``From'' header field. +.Pp +.It Fn contain_subject string +Messages that contain the +.Fa string +.Pq Vt string +in the ``Subject'' header field. +.Pp +.It Fn contain_to string +Messages that contain the +.Fa string +.Pq Vt string +in the ``To'' header field. +.Pp +.It Fn contain_field field string +Messages that contain the +.Fa string +.Pq Vt string +in the +.Fa field +.Pq Vt string +header field. +.Pp +.It Fn contain_body string +Messages that contain the +.Fa string +.Pq Vt string +in the message body. +.Pp +.It Fn contain_message string +Messages that contain the +.Fa string +.Pq Vt string +in the message. +.El +.Pp +The following methods can be used to search for messages that match a specific +regular expression pattern. +.Pp +This way of searching is not supported by the IMAP protocol, and this means +that what actually happens under the hood, is that the relevant parts of all +the messages are downloaded and matched locally. It is therefore recommended +to use these methods with meta-searching (see following section), in order to +narrow down the set of messages that should be searched, and thus minimize what +will be downloaded. +.Pp +Note that due to Lua using backslash `\\' as an escape character for its +strings, one has to double backslashes in order to insert a single backslash +inside a regular expression pattern: +.Pp +.Bl -tag -width Ds -compact +.It Fn match_bcc pattern +Messages that match the regular expression +.Fa pattern +.Pq Vt string +in the ``Bcc'' header field. +.Pp +.It Fn match_cc pattern +Messages that match the regular expression +.Fa pattern +.Pq Vt string +in the ``Cc'' header field. +.Pp +.It Fn match_from pattern +Messages that match the regular expression +.Fa pattern +.Pq Vt string +in the ``From'' header field. +.Pp +.It Fn match_subject pattern +Messages that match the regular expression +.Fa pattern +.Pq Vt string +in the ``Subject'' header field. +.Pp +.It Fn match_to pattern +Messages that match the regular expression +.Fa pattern +.Pq Vt string +in the ``To'' header field. +.Pp +.It Fn match_field field pattern +Messages that match the regular expression +.Fa pattern +.Pq Vt string +in the +.Fa field +.Pq Vt string +header field. +.Pp +.It Fn match_header pattern +Messages that match the regular expression +.Fa pattern +.Pq Vt string +in the message header. +.Pp +.It Fn match_body pattern +Messages that match the regular expression +.Fa pattern +.Pq Vt string +in the message body. +.Pp +.It Fn match_message pattern +Messages that match the regular expression +.Fa pattern +.Pq Vt string +in the message. +.El +.Pp +The following method can be used to search for messages using user queries +based on the IMAP specification (RFC 3501 Section 6.4.4): +.Pp +.Bl -tag -width Ds -compact +.It Fn send_query criteria +Searches messages by sending an IMAP search query as described in the +search +.Fa criteria +.Pq Vt string . +.El +.Pp +Examples: +.Bd -literal -offset 4n +results = myaccount.mymailbox:select_all() +results = myaccount.mymailbox:is_new() +results = myaccount.mymailbox:is_recent() +results = myaccount.mymailbox:is_larger(100000) +results = myaccount.mymailbox:is_older(10) +results = myaccount.mymailbox:has_flag('MyFlag') +results = myaccount.mymailbox:arrived_before('01-Jan-2007') +results = myaccount.mymailbox:sent_since('01-Jan-2007') +results = myaccount.mymailbox:contain_subject('test') +results = myaccount.mymailbox:contain_field('Sender', 'user@host') +results = myaccount.mymailbox:contain_body('hello world') +results = myaccount.mymailbox:match_from('.*(user1|user2)@host') +results = myaccount.mymailbox:send_query('ALL') + +results = myaccount['mymailbox']:is_new() +results = myaccount['myfolder/mymailbox']:is_recent() +.Ed +.Sh RESULTS +After one of more searching methods have been applied to one or more mailboxes, +the result contains all the necessary information, such as which messages +matched in which mailboxes. Using this result these messages can be either +searched further or processed in various way. +.Ss META-SEARCHING +The results of the searching methods can be searched further on in the same way +as searching is done in mailboxes. The difference is that instead of doing the +search in the whole mailbox, ie. in all the messages, it is instead done only +to those messages that were returned in a previous search. +.Pp +Examples: +.Bd -literal -offset 4n +results:match_message('^[Hh]ello world!?$') +myaccount.mymailbox:is_new():match_body('^[Ww]orld, hello!?$') +.Ed +.Ss PROCESSING +The processing methods are applied to the results that searching returned. +.Pp +The following method can be used to delete messages in a mailbox: +.Pp +.Bl -tag -width Ds -compact +.It Fn delete_messages +Deletes the messages that matched. +.El +.Pp +The following methods can be used to copy and move messages in a mailbox at the +same or different accounts. If the destination mailbox is in a different +account than the source mailbox, then the messages are downloaded and then +uploaded to the destination: +.Pp +.Bl -tag -width Ds -compact +.It Fn copy_messages destination +Copies the messages to the +.Fa destination , +which is a mailbox at an account. +.Pp +.It Fn move_messages destination +Moves the messages to the +.Fa destination , +which is a mailbox at an account. +.El +.Pp +The following methods can be used to mark messages in a mailbox: +.Pp +.Bl -tag -width Ds -compact +.It Fn mark_answered +Marks the messages as answered. +.Pp +.It Fn mark_deleted +Marks the messages for later removal. +.Pp +.It Fn mark_draft +Marks the messages as draft. +.Pp +.It Fn mark_flagged +Marks the messages for urgent/special attention. +.Pp +.It Fn mark_seen +Marks the messages as read. +.Pp +.It Fn unmark_answered +Unmarks the messages that have been marked as answered. +.Pp +.It Fn unmark_deleted +Unmarks the messages that have been marked for later removal. +.Pp +.It Fn unmark_draft +Unmarks the messages that have been marked as draft. +.Pp +.It Fn unmark_flagged +Unmarks the messages that have been marked for urgent/special attention. +.Pp +.It Fn unmark_seen +Unmarks the messages that have been marked as read. +.Pp +.El +.Pp +The following methods can be used to flag messages in a mailbox. The standard +system flags are ``\\Answered'', ``\\Deleted'', ``\\Draft'', ``\\Flagged'', +``\\Seen'', while if the server supports it, new user keywords may be defined: +.Pp +.Bl -tag -width Ds -compact +.It Fn add_flags flags +Adds the +.Fa flags +.Po Vt table +that contains +.Vt strings Pc +to the messages. +.Pp +.It Fn remove_flags flags +Removes the +.Fa flags +.Po Vt table +that contains +.Vt strings Pc +from the messages. +.Pp +.It Fn replace_flags flags +Replaces the +.Fa flags +.Po Vt table +that contains +.Vt strings Pc +of the messages. +.El +.Pp +Examples: +.Bd -literal -offset 4n +results:delete_messages() +results:copy_messages(myaccount.othermailbox) +results:move_messages(otheraccount.mymailbox) +results:mark_seen() +results:unmark_flagged() +results:add_flags({ 'MyFlag', '\\\\Seen' }) +results:remove_flags({ '\\\\Seen' }) + +results:move_messages(otheraccount['myfolder/mymailbox']) +.Ed +.Sh MESSAGES +The messages that are residing in any mailbox can be also accessed, as a whole +or in parts. Messages can be accessed using their unique identifier (UID): +.Bd -literal -offset 4n +myaccount.mymailbox[22] +.Ed +.Pp +The UIDs of messages the user is interested in, are gained from the results of +searching: +.Bd -literal -offset 4n +results = account.INBOX:is_unread() +for _, message in ipairs(results) do + mailbox, uid = unpack(message) + header = mailbox[uid]:fetch_header() +end +.Ed +.Ss FETCHING +.Pp +The following methods can be used to fetch parts of messages. The methods +return a +.Vt string . +The downloaded message parts are cached locally, so they can be reused inside +the same program session: +.Pp +.Bl -tag -width Ds -compact +.It Fn fetch_message +Fetches the header and body of the message. +.Pp +.It Fn fetch_header +Fetches the header of the message. +.Pp +.It Fn fetch_body +Fetches the body of the messages. +.Pp +.It Fn fetch_field field +Fetches the specified header +.Fa field +.Pq Vt string +of the message. +.Pp +.It Fn fetch_part part +Fetches the specified +.Fa part +.Po +.Vt string +.Pc +of the message. +.El +.Pp +The following methods can be used to fetch details about the state of a +message: +.Pp +.Bl -tag -width Ds -compact +.It Fn fetch_flags +Fetches the flags of the message. Returns a +.Vt table , +of +.Vt strings . +.Pp +.It Fn fetch_date +Fetches the internal date of the message. Returns a +.Vt string . +.Pp +.It Fn fetch_size +Fetches the size of the message. Returns a +.Vt number . +.Pp +.It Fn fetch_structure +Fetches the body structure of the message. Returns a +.Vt table +that has as keys the parts of the message, and as values a +.Vt table +that has one mandatory element, the type +.Pq Vt string +of the part, and two +optional elements, +the size +.Pq Vt number +and name +.Pq Vt string +of the part. +.El +.Pp +Examples: +.Bd -literal -offset 4n +myaccount.mymailbox[2]:fetch_message() +myaccount.mymailbox[3]:fetch_field('subject') +myaccount.mymailbox[5]:fetch_part('1.1') + +myaccount['mymailbox'][7]:fetch_message() +myaccount['myfolder/mymailbox'][11]:fetch_message() +.Ed +.Sh FUNCTIONS +The following auxiliary functions are also available for convenience: +.Pp +.Bl -tag -width Ds -compact +.It Fn form_date days +Forms a date in ``day-month-year'' format that the system had before the number of +.Fa days +.Pq Vt number , +and returns it as a +.Vt string . +.Pp +.It Fn get_password prompt +Displays the specified +.Fa prompt +.Pq Vt string , +and reads a password, while character echoing is turned off. Returns +that password as a +.Vt string . +.Pp +.It Fn become_daemon interval commands +Detaches the program from the controlling terminal and runs it in the +background as system daemon. The program will then repeatedly poll at the +specified +.Fa interval +.Pq Vt number +in seconds. Each time the program wakes up, the +.Fa commands +.Pq Vt function +are executed. +.Pp +.It Fn pipe_to command data +Executes the system's +.Fa command +.Pq Vt string +and sends the +.Fa data +.Pq Vt string +to the standard input channel of the subprocess. Returns a +.Vt number , +the exit status of the child process. +.Pp +.It Fn pipe_from command +Executes the system's +.Fa command +.Pq Vt string +and retrieves the data from the standard output channel of the subprocess. +Returns a +.Vt number , +the exit status of the child process, and a +.Vt string , +the output of the child process. +.Pp +.It Fn regex_search pattern string +Implements Perl-compatible regular expressions (PCRE). The +.Fa pattern +.Pq Vt string +is a PCRE pattern. The +.Vt string +.Pq Vt string +is the subject string in which the pattern is +matched against. Returns at least a +.Vt boolean , +that denotes if the match was successful, and any captures which are of +.Vt string +type. Note that due to Lua using backslash `\\' as an escape character for its +strings, one has to double backslashes in order to insert a single backslash +inside a regular expression pattern: +.El +.Pp +Examples: +.Bd -literal -offset 4n +date = form_date(14) +password = get_password('Enter password: ') +become_daemon(600, myfunction) +status = pipe_to('mycommandline', 'mydata') +status, data = pipe_from('mycommandline') +success, capture = regex_search('^[PpCcRrEe]: (\\\\w)$', 'mystring') +.Ed +.Sh EXAMPLES +See +.Pa sample.config.lua +and +.Pa sample.extend.lua . +.Sh ENVIRONMENT +.Bl -tag -width Ds +.It Ev HOME +User's home directory. +.El +.Sh SEE ALSO +.Xr imapfilter 1 +.Sh BUGS +.Pp +Since version 2.2, a different format is used for the returned structures of +the searching methods, due to the introduction of multiple mailbox searching +and meta-searching, and thus any configuration files that rely on them should +be updated. Consequently, the processing and fetching methods have been also +enhanced and the relevant documentation modified, and while these changes are +backwards compatible, an update of the configuration file is still recommended. +.Pp +Since version 2.0, the configuration file format has changed. The new format +is not backwards compatible, and thus it should not be mixed with the old +format. Nevertheless, configuration files that employ the old, and now +deprecated, format can still be read and executed as before. diff --git a/list.c b/list.c new file mode 100644 index 0000000..52764c1 --- /dev/null +++ b/list.c @@ -0,0 +1,59 @@ +#include + +#include "imapfilter.h" +#include "list.h" + + +/* + * Add a new element at the end of the list. + */ +list * +list_append(list *lst, void *data) +{ + list *l, *nl; + + nl = (list *)xmalloc(sizeof(list)); + nl->data = data; + nl->prev = nl->next = NULL; + + if (lst != NULL) { + for (l = lst; l->next != NULL; l = l->next); + l->next = nl; + nl->prev = l; + + return lst; + } else { + return nl; + } +} + + +/* + * Remove an element from the list. + */ +list * +list_remove(list *lst, void *data) +{ + list *l; + + l = lst; + while (l != NULL) { + if (l->data != data) + l = l->next; + else { + if (l->prev) + l->prev->next = l->next; + if (l->next) + l->next->prev = l->prev; + if (lst == l) + lst = lst->next; + + xfree(l); + + break; + } + } + + return lst; +} + diff --git a/list.h b/list.h new file mode 100644 index 0000000..5da9d75 --- /dev/null +++ b/list.h @@ -0,0 +1,16 @@ +#ifndef LIST_H +#define LIST_H + + +typedef struct list { + void *data; + struct list *next, *prev; +} list; + + +/* list.h */ +list *list_append(list *lst, void *data); +list *list_remove(list *lst, void *data); + + +#endif /* LIST_H */ diff --git a/log.c b/log.c new file mode 100644 index 0000000..30ec284 --- /dev/null +++ b/log.c @@ -0,0 +1,240 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "imapfilter.h" +#include "session.h" +#include "list.h" +#include "pathnames.h" + + +extern options opts; +extern environment env; +extern unsigned int flags; +extern list *sessions; + +static FILE *debugfp = NULL; /* Pointer to debug file. */ +static FILE *logfp = NULL; /* Pointer to log file. */ + + +char *log_time(void); + + +/* + * Print message if in verbose mode. + */ +void +verbose(const char *fmt,...) +{ + va_list args; + + if (!opts.verbose) + return; + + va_start(args, fmt); + vprintf(fmt, args); + va_end(args); +} + + +/* + * Write message to debug file. + */ +void +debug(const char *fmt,...) +{ + va_list args; + + if (opts.debug <= 0 || !debugfp) + return; + + va_start(args, fmt); + vfprintf(debugfp, fmt, args); + fflush(debugfp); + + va_end(args); +} + +/* + * Write character to debug file. + */ +void +debugc(char c) +{ + + if (opts.debug <= 0 || !debugfp) + return; + + fputc(c, debugfp); +} + + +/* + * Print error message and write it into log file. + */ +void +error(const char *fmt,...) +{ + va_list args; + + va_start(args, fmt); + fprintf(stderr, "imapfilter: "); + vfprintf(stderr, fmt, args); + va_end(args); + if (logfp) { + va_start(args, fmt); + fprintf(logfp, "%s: ", log_time()); + vfprintf(logfp, fmt, args); + fflush(logfp); + va_end(args); + } +} + + +/* + * Print error message and exit program. + */ +void +fatal(unsigned int errnum, const char *fmt,...) +{ + va_list args; + list *l; + session *s; + + va_start(args, fmt); + fprintf(stderr, "imapfilter: "); + vfprintf(stderr, fmt, args); + va_end(args); + + if (logfp) { + va_start(args, fmt); + fprintf(logfp, "%s: ", log_time()); + vfprintf(logfp, fmt, args); + fflush(logfp); + va_end(args); + } + + for (l = sessions; l; l = l->next) { + s = l->data; + close_connection(s); + } + + close_log(); + close_debug(); + + exit(errnum); +} + + +/* + * Open temporary debug file and associate a stream with the returned file + * descriptor. + */ +int +open_debug(void) +{ + int n; + char b; + char *dt; + int fd; + + if (!opts.debug) + return 0; + + n = snprintf(&b, 1, "%s/%s", env.home, PATHNAME_DEBUG); + + if (env.pathmax != -1 && n > env.pathmax) + fatal(ERROR_PATHNAME, + "pathname limit %ld exceeded: %d\n", env.pathmax, n); + + dt = (char *)xmalloc((n + 1) * sizeof(char)); + snprintf(dt, n + 1, "%s/%s", env.home, PATHNAME_DEBUG); + + fd = mkstemp(dt); + + if (fd != -1) { + debugfp = fdopen(fd, "w"); + if (debugfp == NULL) { + error("opening debug file %s: %s\n", dt, + strerror(errno)); + return -1; + } + } + return 0; +} + + +/* + * Close temporary debug file. + */ +int +close_debug(void) +{ + + if (debugfp == NULL) + return 0; + else + return fclose(debugfp); +} + + +/* + * Open the file for saving of logging information. + */ +int +open_log(void) +{ + + if (opts.log == NULL) + return 0; + + debug("log file: '%s'\n", opts.log); + + if (create_file(opts.log, S_IRUSR | S_IWUSR)) + return 1; + + logfp = fopen(opts.log, "a"); + if (logfp == NULL) { + error("opening log file %s: %s\n", opts.log, strerror(errno)); + return 1; + } + return 0; +} + + +/* + * Close the log file. + */ +int +close_log(void) +{ + + if (logfp == NULL) + return 0; + else + return fclose(logfp); +} + + +/* + * Return current local time and date. + */ +char * +log_time(void) +{ + char *ct; + time_t t; + + t = time(NULL); + + ct = ctime(&t); + *(strchr(ct, '\n')) = '\0'; + + return ct; +} diff --git a/lua.c b/lua.c new file mode 100644 index 0000000..659ac17 --- /dev/null +++ b/lua.c @@ -0,0 +1,343 @@ +#include +#include +#include + +#include +#include +#include + +#include "imapfilter.h" +#include "pathnames.h" + + +extern options opts; +extern struct sessionhead sessions; + +static lua_State *lua; /* Lua interpreter state. */ + + +void init_options(void); +void interactive_mode(void); + + +/* + * Start the Lua interpreter, export IMAP core and system functions, load the + * Lua interface functions, load and execute imapfilter's configuration file. + */ +void +start_lua() +{ + + lua = luaL_newstate(); + + luaL_openlibs(lua); + + luaopen_ifcore(lua); + luaopen_ifsys(lua); + luaopen_ifre(lua); + + lua_settop(lua, 0); + + init_options(); + + if (luaL_loadfile(lua, PATHNAME_COMMON) || + lua_pcall(lua, 0, LUA_MULTRET, 0)) + fatal(ERROR_CONFIG, "%s\n", lua_tostring(lua, -1)); + + if (luaL_loadfile(lua, PATHNAME_SET) || + lua_pcall(lua, 0, LUA_MULTRET, 0)) + fatal(ERROR_CONFIG, "%s\n", lua_tostring(lua, -1)); + + if (luaL_loadfile(lua, PATHNAME_REGEX) || + lua_pcall(lua, 0, LUA_MULTRET, 0)) + fatal(ERROR_CONFIG, "%s\n", lua_tostring(lua, -1)); + + if (luaL_loadfile(lua, PATHNAME_ACCOUNT) || + lua_pcall(lua, 0, LUA_MULTRET, 0)) + fatal(ERROR_CONFIG, "%s\n", lua_tostring(lua, -1)); + + if (luaL_loadfile(lua, PATHNAME_MAILBOX) || + lua_pcall(lua, 0, LUA_MULTRET, 0)) + fatal(ERROR_CONFIG, "%s\n", lua_tostring(lua, -1)); + + if (luaL_loadfile(lua, PATHNAME_MESSAGE) || + lua_pcall(lua, 0, LUA_MULTRET, 0)) + fatal(ERROR_CONFIG, "%s\n", lua_tostring(lua, -1)); + + if (luaL_loadfile(lua, PATHNAME_OPTIONS) || + lua_pcall(lua, 0, LUA_MULTRET, 0)) + fatal(ERROR_CONFIG, "%s\n", lua_tostring(lua, -1)); + + if (luaL_loadfile(lua, PATHNAME_AUXILIARY) || + lua_pcall(lua, 0, LUA_MULTRET, 0)) + fatal(ERROR_CONFIG, "%s\n", lua_tostring(lua, -1)); + + if (luaL_loadfile(lua, PATHNAME_DEPRECATED) || + lua_pcall(lua, 0, LUA_MULTRET, 0)) + fatal(ERROR_CONFIG, "%s\n", lua_tostring(lua, -1)); + + if (opts.oneline != NULL) { + if (luaL_loadbuffer(lua, opts.oneline, strlen(opts.oneline), + "=") || lua_pcall(lua, 0, LUA_MULTRET, 0)) + fatal(ERROR_CONFIG, "%s\n", lua_tostring(lua, -1)); + } else { + if (luaL_loadfile(lua, opts.config) || + lua_pcall(lua, 0, LUA_MULTRET, 0)) + fatal(ERROR_CONFIG, "%s\n", lua_tostring(lua, -1)); + } + + if (opts.interactive) + interactive_mode(); +} + + +/* + * Stop the Lua interpreter. + */ +void +stop_lua(void) +{ + + lua_close(lua); +} + + +/* + * Set default values to program's options. + */ +void +init_options(void) +{ + + lua_newtable(lua); + + set_table_boolean("certificates", 1); + set_table_boolean("crammd5", 1); + set_table_boolean("create", 0); + set_table_boolean("expunge", 1); + set_table_number("keepalive", 29); + set_table_boolean("namespace", 1); + set_table_boolean("starttls", 1); + set_table_boolean("subscribe", 0); + set_table_number("timeout", 0); + + lua_setglobal(lua, "options"); +} + + +/* + * Interactive mode. + */ +void +interactive_mode(void) +{ + char buf[LINE_MAX]; + + for (;;) { + printf("> "); + fflush(stdout); + + if (fgets(buf, sizeof(buf), stdin) == NULL) { + printf("\n"); + break; + } + + if (luaL_loadbuffer(lua, buf, strlen(buf), "=") || + lua_pcall(lua, 0, LUA_MULTRET, 0)) { + error("%s\n", lua_tostring(lua, -1)); + lua_pop(lua, 1); + } + } +} + + +/* + * Get from the configuration file the value of a boolean option variable. + */ +int +get_option_boolean(const char *opt) +{ + int b; + + lua_pushstring(lua, "options"); + lua_gettable(lua, LUA_GLOBALSINDEX); + if (!lua_istable(lua, -1)) { + lua_pop(lua, 1); + return 0; + } + lua_pushstring(lua, opt); + lua_gettable(lua, -2); + + b = lua_toboolean(lua, -1); + + lua_pop(lua, 2); + + return b; +} + + +/* + * Get from the configuration file the value of a number option variable. + */ +lua_Number +get_option_number(const char *opt) +{ + lua_Number n; + + lua_pushstring(lua, "options"); + lua_gettable(lua, LUA_GLOBALSINDEX); + if (!lua_istable(lua, -1)) { + lua_pop(lua, 1); + return 0; + } + lua_pushstring(lua, opt); + lua_gettable(lua, -2); + + n = lua_tonumber(lua, -1); + + lua_pop(lua, 2); + + return n; +} + + +/* + * Get from the configuration file the value of a string option variable. + */ +const char * +get_option_string(const char *opt) +{ + const char *s; + + lua_pushstring(lua, "options"); + lua_gettable(lua, LUA_GLOBALSINDEX); + if (!lua_istable(lua, -1)) { + lua_pop(lua, 1); + return NULL; + } + lua_pushstring(lua, opt); + lua_gettable(lua, -2); + + s = lua_tostring(lua, -1); + + lua_pop(lua, 2); + + return s; +} + + +/* + * Get the type of a table's element. + */ +int +get_table_type(const char *key) +{ + int t; + + lua_pushstring(lua, key); + lua_gettable(lua, -2); + + t = lua_type(lua, -1); + + lua_pop(lua, 1); + + return t; +} + + +/* + * Get the value of a table's element of type number. + */ +lua_Number +get_table_number(const char *key) +{ + lua_Number n; + + lua_pushstring(lua, key); + lua_gettable(lua, -2); + + n = lua_tonumber(lua, -1); + + lua_pop(lua, 1); + + return n; +} + + +/* + * Get the value of a table's element of type string. + */ +const char * +get_table_string(const char *key) +{ + const char *s; + + lua_pushstring(lua, key); + lua_gettable(lua, -2); + + s = lua_tostring(lua, -1); + + lua_pop(lua, 1); + + return s; +} + + +/* + * Set a table's element value to nil. + */ +int +set_table_nil(const char *key) +{ + + lua_pushstring(lua, key); + lua_pushnil(lua); + lua_settable(lua, -3); + + return 0; +} + + +/* + * Set a table's element value to the specified boolean. + */ +int +set_table_boolean(const char *key, int value) +{ + + lua_pushstring(lua, key); + lua_pushboolean(lua, value); + lua_settable(lua, -3); + + return 0; +} + + +/* + * Set a table's element value to the specified number. + */ +int +set_table_number(const char *key, lua_Number value) +{ + + lua_pushstring(lua, key); + lua_pushnumber(lua, value); + lua_settable(lua, -3); + + return 0; +} + + +/* + * Set a table's element value to the specified string. + */ +int +set_table_string(const char *key, const char *value) +{ + + lua_pushstring(lua, key); + lua_pushstring(lua, value); + lua_settable(lua, -3); + + return 0; +} diff --git a/mailbox.lua b/mailbox.lua new file mode 100644 index 0000000..0dd781e --- /dev/null +++ b/mailbox.lua @@ -0,0 +1,1243 @@ +-- The Mailbox class represents a mailbox that resides in an IMAP account. + +Mailbox = {} + +Mailbox._mt = {} +setmetatable(Mailbox, Mailbox._mt) + + +Mailbox._mt.__call = function (self, account, mailbox) + local object = {} + + object._type = 'mailbox' + + for key, value in pairs(Mailbox) do + if (type(value) == 'function') then + object[key] = value + end + end + + object._mt = {} + object._mt.__index = object._attach_message + setmetatable(object, object._mt) + + object._account = account + object._mailbox = mailbox + + return object +end + + +function Mailbox._attach_message(self, uid) + self[uid] = Message(self._account, self, uid) + return self[uid] +end + +function Mailbox._detach_message(self, uid) + self[uid] = nil +end + + +function Mailbox._cached_select(self, account, mbox) + if (account._mailbox == nil or account._mailbox ~= mbox) then + if (ifcore.select(self._account._imap, mbox) == true) then + account._mailbox = mbox + return true + else + return false + end + else + return true + end +end + +function Mailbox._cached_close(self, account) + account._mailbox = nil + return ifcore.close(self._account._imap) +end + + +function Mailbox._send_query(self, criteria, charset) + _check_optional(criteria, { 'string', 'table' }) + _check_optional(charset, 'string') + + local query + if (criteria == nil) then + query = 'ALL' + elseif (type(criteria) == 'string') then + query = 'ALL ' .. criteria + else + query = _make_query(criteria) + end + + if (charset == nil) then + if (type(options) == 'table' and type(options.charset) == 'string') then + charset = options.charset + else + charset = '' + end + end + + if (self._account._login_user(self._account) ~= true) then + return {} + end + + if (self._cached_select(self, self._account._imap, self._mailbox) ~= + true) then + return {} + end + + local _, results = ifcore.search(self._account._imap, query, charset) + + if (type(options) == 'table' and options.close == true) then + self._cached_close(self, self._account._imap) + end + + if (results == nil) then + return {} + end + + local t = {} + for n in string.gmatch(results, '%d+') do + table.insert(t, { self, tonumber(n) }) + end + + return t +end + + +function Mailbox._flag_messages(self, mode, flags, messages) + if (not messages or #messages == 0) then + return + end + + if (self._account._login_user(self._account) ~= true) then + return + end + + if (self._cached_select(self, self._account._imap, self._mailbox) ~= + true) then + return + end + + local f = '' + if (#flags ~= 0) then + f = table.concat(flags, ' ') + end + + local m = _make_range(messages) + local n = #m + local r = false + for i = 1, n, 50 do + j = i + 49 + if (n < j) then + j = n + end + r = ifcore.store(self._account._imap, table.concat(m, ',', i, j), + mode, f) + if r == false then + break + end + end + + if (type(options) == 'table' and options.close == true) then + self._cached_close(self, self._account._imap) + end + + return r +end + + +function Mailbox._copy_messages(self, dest, messages) + if (not messages or #messages == 0) then + return + end + + if (self._account._login_user(self._account) ~= true) then + return + end + + local r = false + if (self._account._imap == dest._account._imap) then + if (self._cached_select(self, self._account._imap, self._mailbox) ~= true) then + return + end + + local m = _make_range(messages) + local n = #m + for i = 1, n, 50 do + j = i + 49 + if (n < j) then + j = n + end + r = ifcore.copy(self._account._imap, table.concat(m, ',', i, j), + dest._mailbox) + if r == false then + break + end + end + + if (type(options) == 'table' and options.close == true) then + self._cached_close(self, self._account._imap) + end + else + local fast = self._fetch_fast(self, messages) + local mesgs = self._fetch_message(self, messages) + + if (dest._account._login_user(dest._account) ~= true) then + return + end + + for i in pairs(fast) do + for k, v in ipairs(fast[i]['flags']) do + if (string.lower(v) == '\\recent') then + table.remove(fast[i]['flags'], k) + end + end + + r = ifcore.append(dest._account._imap, dest._mailbox, mesgs[i], + table.concat(fast[i]['flags'], ' '), fast[i]['date']) + end + end + + return r +end + + +function Mailbox._fetch_fast(self, messages) + if (not messages or #messages == 0) then + return + end + + if (self._account._login_user(self._account) ~= true) then + return + end + + if (self._cached_select(self, self._account._imap, self._mailbox) ~= + true) then + return + end + + local results = {} + for _, m in ipairs(messages) do + local _, flags, date, size = ifcore.fetchfast(self._account._imap, + tostring(m)) + if (flags ~= nil and date ~= nil and size ~= nil ) then + local f = {} + for s in string.gmatch(flags, '%S+') do + table.insert(f, s) + end + results[m] = {} + results[m]['flags'] = f + results[m]['date'] = date + results[m]['size'] = size + end + end + + if (type(options) == 'table' and options.close == true) then + self._cached_close(self, self._account._imap) + end + + return results +end + +function Mailbox._fetch_flags(self, messages) + if (not messages or #messages == 0) then + return + end + + if (self._account._login_user(self._account) ~= true) then + return + end + + if (self._cached_select(self, self._account._imap, self._mailbox) ~= + true) then + return + end + + local results = {} + for _, m in ipairs(messages) do + local _, flags = ifcore.fetchflags(self._account._imap, tostring(m)) + if (flags ~= nil) then + local f = {} + for s in string.gmatch(flags, '%S+') do + table.insert(f, s) + end + results[m] = f + end + end + + if (type(options) == 'table' and options.close == true) then + self._cached_close(self, self._account._imap) + end + + return results +end + +function Mailbox._fetch_date(self, messages) + if (not messages or #messages == 0) then + return + end + + if (self._account._login_user(self._account) ~= true) then + return + end + + if (self._cached_select(self, self._account._imap, self._mailbox) ~= + true) then + return + end + + local results = {} + for _, m in ipairs(messages) do + if (type(options) == 'table' and options.cache == true and + self[m]._date) then + results[m] = self[m]._date + else + local _, date = ifcore.fetchdate(self._account._imap, tostring(m)) + if (date ~= nil) then + results[m] = date + if (type(options) == 'table' and options.cache == true) then + self[m]._date = date + end + end + end + end + + if (type(options) == 'table' and options.close == true) then + self._cached_close(self, self._account._imap) + end + + return results +end + +function Mailbox._fetch_size(self, messages) + if (not messages or #messages == 0) then + return + end + + if (self._account._login_user(self._account) ~= true) then + return + end + + if (self._cached_select(self, self._account._imap, self._mailbox) ~= + true) then + return + end + + local results = {} + for _, m in ipairs(messages) do + if (type(options) == 'table' and options.cache == true and + self[m]._size) then + results[m] = self[m]._size + else + local _, size = ifcore.fetchsize(self._account._imap, tostring(m)) + if (size ~= nil) then + results[m] = tonumber(size) + if (type(options) == 'table' and options.cache == true) then + self[m]._size = tonumber(size) + end + end + end + end + + if (type(options) == 'table' and options.close == true) then + self._cached_close(self, self._account._imap) + end + + return results +end + +function Mailbox._fetch_header(self, messages) + if (not messages or #messages == 0) then + return + end + + if (self._account._login_user(self._account) ~= true) then + return + end + + if (self._cached_select(self, self._account._imap, self._mailbox) ~= + true) then + return + end + + local results = {} + for _, m in ipairs(messages) do + if (type(options) == 'table' and options.cache == true and + self[m]._header) then + results[m] = self[m]._header + else + local _, header = ifcore.fetchheader(self._account._imap, + tostring(m)) + if (header ~= nil) then + results[m] = header + if (type(options) == 'table' and options.cache == true) then + self[m]._header = header + end + end + end + end + + if (type(options) == 'table' and options.close == true) then + self._cached_close(self, self._account._imap) + end + + return results +end + +function Mailbox._fetch_body(self, messages) + if (not messages or #messages == 0) then + return + end + + if (self._account._login_user(self._account) ~= true) then + return + end + + if (self._cached_select(self, self._account._imap, self._mailbox) ~= + true) then + return + end + + local results = {} + for _, m in ipairs(messages) do + if (type(options) == 'table' and options.cache == true and + self[m]._body) then + results[m] = self[m]._body + else + local _, body = ifcore.fetchbody(self._account._imap, tostring(m)) + if (body ~= nil) then + results[m] = body + if (type(options) == 'table' and options.cache == true) then + self[m]._body = body + end + end + end + end + + if (type(options) == 'table' and options.close == true) then + self._cached_close(self, self._account._imap) + end + + return results +end + +function Mailbox._fetch_message(self, messages) + if (not messages or #messages == 0) then + return + end + + local header = self._fetch_header(self, messages) + local body = self._fetch_body(self, messages) + + local results = {} + for _, m in ipairs(messages) do + if (header[m] == nil) then + results[m] = nil + elseif (body[m] == nil) then + results[m] = header[m] + else + results[m] = header[m] .. body[m] + end + end + + return results +end + + +function Mailbox._fetch_fields(self, fields, messages) + if (not messages or #messages == 0) then + return + end + + if (self._account._login_user(self._account) ~= true) then + return + end + + if (self._cached_select(self, self._account._imap, self._mailbox) ~= + true) then + return + end + + local results = {} + for _, m in ipairs(messages) do + results[m] = '' + for _, f in ipairs(fields) do + if (type(options) == 'table' and options.cache == true and + self[m]._fields[f]) then + results[m] = results[m] .. self[m]._fields[f] + else + local _, field = ifcore.fetchfields(self._account._imap, + tostring(m), f) + if (field ~= nil) then + field = string.gsub(field, "\r\n\r\n$", "\n") + results[m] = results[m] .. field + if (type(options) == 'table' and options.cache == true) then + self[m]._fields[f] = field + end + end + end + end + results[m] = string.gsub(results[m], "\n$", "") + end + + if (type(options) == 'table' and options.close == true) then + self._cached_close(self, self._account._imap) + end + + return results +end + +function Mailbox._fetch_structure(self, messages) + if (not messages or #messages == 0) then + return + end + + if (self._account._login_user(self._account) ~= true) then + return + end + + if (self._cached_select(self, self._account._imap, self._mailbox) ~= + true) then + return + end + + local results = {} + for _, m in ipairs(messages) do + if (type(options) == 'table' and options.cache == true and + self[m]._structure) then + results[m] = self[m]._structure + else + local _, structure = ifcore.fetchstructure(self._account._imap, + tostring(m)) + if (structure ~= nil) then + local parsed = _parse_structure({ ['s'] = structure, ['i'] = 1 }) + results[m] = parsed + if (type(options) == 'table' and options.cache == true) then + self[m]._structure = parsed + end + end + end + end + + if (type(options) == 'table' and options.close == true) then + self._cached_close(self, self._account._imap) + end + + return results +end + +function Mailbox._fetch_parts(self, parts, message) + if (self._account._login_user(self._account) ~= true) then + return + end + + if (self._cached_select(self, self._account._imap, self._mailbox) ~= + true) then + return + end + + local results = {} + for _, part in ipairs(parts) do + results[part] = '' + if (type(options) == 'table' and options.cache == true and + self[message]._parts[part]) then + results[part] = self[message]._parts[part] + else + local _, bodypart = ifcore.fetchpart(self._account._imap, + tostring(message), part) + if (bodypart ~= nil) then + results[part] = bodypart + self[message]._parts[part] = bodypart + end + end + end + + if (type(options) == 'table' and options.close == true) then + self._cached_close(self, self._account._imap) + end + + return results +end + + +function Mailbox.check_status(self) + if (self._account._login_user(self._account) ~= true) then + return + end + + local _, exist, recent, unseen, uidnext = ifcore.status(self._account._imap, + self._mailbox) + + if (type(options) == 'table' and options.info == true) then + print(string.format("%d messages, %d recent, %d unseen, in %s@%s/%s.", + exist, recent, unseen, + self._account._imap.username, + self._account._imap.server, self._mailbox)) + end + + return exist, recent, unseen, uidnext +end + + +function Mailbox.send_query(self, criteria, charset) + return Set(self._send_query(self, criteria, charset)) +end + +function Mailbox.select_all(self) + return self.send_query(self) +end + + +function Mailbox.add_flags(self, flags, messages) + _check_required(flags, 'table') + _check_required(messages, 'table') + + local mesgs = _extract_messages(self, messages) + + local r = self._flag_messages(self, 'add', flags, mesgs) + + if (type(options) == 'table' and options.info == true and r == true) then + print(string.format("%d messages flagged in %s@%s/%s.", + #mesgs, self._account._imap.username, + self._account._imap.server, self._mailbox)) + end + + return r +end + +function Mailbox.remove_flags(self, flags, messages) + _check_required(flags, 'table') + _check_required(messages, 'table') + + local mesgs = _extract_messages(self, messages) + + local r = self._flag_messages(self, 'remove', flags, mesgs) + + if (type(options) == 'table' and options.info == true and r == true) then + print(string.format("%d messages flagged in %s@%s/%s.", + #mesgs, self._account._imap.username, + self._account._imap.server, self._mailbox)) + end + + return r +end + +function Mailbox.replace_flags(self, flags, messages) + _check_required(flags, 'table') + _check_required(messages, 'table') + + local mesgs = _extract_messages(self, messages) + + local r = self._flag_messages(self, 'replace', flags, mesgs) + + if (type(options) == 'table' and options.info == true and r == true) then + print(string.format("%d messages flagged in %s@%s/%s.", + #mesgs, self._account._imap.username, + self._account._imap.server, self._mailbox)) + end + + return r +end + + +function Mailbox.mark_answered(self, messages) + _check_required(messages, 'table') + + local mesgs = _extract_messages(self, messages) + + local r = self._flag_messages(self, 'add', { '\\Answered' }, mesgs) + + if (type(options) == 'table' and options.info == true and r == true) then + print(string.format("%d messages marked answered in %s@%s/%s.", + #mesgs, self._account._imap.username, + self._account._imap.server, self._mailbox)) + end + + return r +end + + +function Mailbox.mark_deleted(self, messages) + _check_required(messages, 'table') + + local mesgs = _extract_messages(self, messages) + + local r = self._flag_messages(self, 'add', { '\\Deleted' }, mesgs) + + if (type(options) == 'table' and options.info == true and r == true) then + print(string.format("%d messages marked deleted in %s@%s/%s.", + #mesgs, self._account._imap.username, + self._account._imap.server, self._mailbox)) + end + + return r +end + +function Mailbox.mark_draft(self, messages) + _check_required(messages, 'table') + + local mesgs = _extract_messages(self, messages) + + local r = self._flag_messages(self, 'add', { '\\Draft' }, mesgs) + + if (type(options) == 'table' and options.info == true and r == true) then + print(string.format("%d messages marked draft in %s@%s/%s.", + #mesgs, self._account._imap.username, + self._account._imap.server, self._mailbox)) + end + + return r +end + +function Mailbox.mark_flagged(self, messages) + _check_required(messages, 'table') + + local mesgs = _extract_messages(self, messages) + + local r = self._flag_messages(self, 'add', { '\\Flagged' }, mesgs) + + if (type(options) == 'table' and options.info == true and r == true) then + print(string.format("%d messages marked flagged in %s@%s/%s.", + #mesgs, self._account._imap.username, + self._account._imap.server, self._mailbox)) + end + + return r +end + +function Mailbox.mark_seen(self, messages) + _check_required(messages, 'table') + + local mesgs = _extract_messages(self, messages) + + local r = self._flag_messages(self, 'add', { '\\Seen' }, mesgs) + + if (type(options) == 'table' and options.info == true and r == true) then + print(string.format("%d messages marked seen in %s@%s/%s.", + #mesgs, self._account._imap.username, + self._account._imap.server, self._mailbox)) + end + + return r +end + +function Mailbox.unmark_answered(self, messages) + _check_required(messages, 'table') + + local mesgs = _extract_messages(self, messages) + + local r = self._flag_messages(self, 'remove', { '\\Answered' }, mesgs) + + if (type(options) == 'table' and options.info == true and r == true) then + print(string.format("%d messages unmarked answered in %s@%s/%s.", + #mesgs, self._account._imap.username, + self._account._imap.server, self._mailbox)) + end + + return r +end + +function Mailbox.unmark_deleted(self, messages) + _check_required(messages, 'table') + + local mesgs = _extract_messages(self, messages) + + local r = self._flag_messages(self, 'remove', { '\\Deleted' }, mesgs) + + if (type(options) == 'table' and options.info == true and r == true) then + print(string.format("%d messages unmarked deleted in %s@%s/%s.", + #mesgs, self._account._imap.username, + self._account._imap.server, self._mailbox)) + end + + return r +end + +function Mailbox.unmark_draft(self, messages) + _check_required(messages, 'table') + + local mesgs = _extract_messages(self, messages) + + local r = self._flag_messages(self, 'remove', { '\\Draft' }, mesgs) + + if (type(options) == 'table' and options.info == true and r == true) then + print(string.format("%d messages unmarked draft in %s@%s/%s.", + #mesgs, self._account._imap.username, + self._account._imap.server, self._mailbox)) + end + + return r +end + +function Mailbox.unmark_flagged(self, messages) + _check_required(messages, 'table') + + local mesgs = _extract_messages(self, messages) + + local r = self._flag_messages(self, 'remove', { '\\Flagged' }, mesgs) + + if (type(options) == 'table' and options.info == true and r == true) then + print(string.format("%d messages unmarked flagged in %s@%s/%s.", + #mesgs, self._account._imap.username, + self._account._imap.server, self._mailbox)) + end + + return r +end + +function Mailbox.unmark_seen(self, messages) + _check_required(messages, 'table') + + local mesgs = _extract_messages(self, messages) + + local r = self._flag_messages(self, 'remove', { '\\Seen' }, mesgs) + + if (type(options) == 'table' and options.info == true and r == true) then + print(string.format("%d messages unmarked seen in %s@%s/%s.", + #mesgs, self._account._imap.username, + self._account._imap.server, self._mailbox)) + end + + return r +end + + +function Mailbox.delete_messages(self, messages) + _check_required(messages, 'table') + + local mesgs = _extract_messages(self, messages) + + local r = self._flag_messages(self, 'add', { '\\Deleted' }, mesgs) + + if (type(options) == 'table' and options.info == true and r == true) then + print(string.format("%d messages deleted in %s@%s/%s.", + #mesgs, self._account._imap.username, + self._account._imap.server, self._mailbox)) + end + + return r +end + + +function Mailbox.copy_messages(self, dest, messages) + _check_required(dest, 'table') + _check_required(messages, 'table') + + local mesgs = _extract_messages(self, messages) + + local r = self._copy_messages(self, dest, mesgs) + + if (type(options) == 'table' and options.info == true and r == true) then + print(string.format("%d messages copied from %s@%s/%s to %s@%s/%s.", + #mesgs, self._account._imap.username, + self._account._imap.server, self._mailbox, + dest._account._imap.username, + dest._account._imap.server, + dest._mailbox)) + end + + return r +end + + +function Mailbox.move_messages(self, dest, messages) + _check_required(dest, 'table') + _check_required(messages, 'table') + + local mesgs = _extract_messages(self, messages) + + local rc = self._copy_messages(self, dest, mesgs) + local rf = false + if (rc == true) then + rf = self._flag_messages(self, 'add', { '\\Deleted' }, mesgs) + end + + if (type(options) == 'table' and options.info == true and + rc == true and rf == true) then + print(string.format("%d messages moved from %s@%s/%s to %s@%s/%s.", + #mesgs, self._account._imap.username, + self._account._imap.server, self._mailbox, + dest._account._imap.username, + dest._account._imap.server, + dest._mailbox)) + end + + return rc == true and rf == true +end + + +function Mailbox.fetch_flags(self, messages) + _check_required(messages, 'table') + + return self._fetch_flags(self, _extract_messages(self, messages)) +end + +function Mailbox.fetch_date(self, messages) + _check_required(messages, 'table') + + return self._fetch_date(self, _extract_messages(self, messages)) +end + +function Mailbox.fetch_size(self, messages) + _check_required(messages, 'table') + + return self._fetch_size(self, _extract_messages(self, messages)) +end + +function Mailbox.fetch_header(self, messages) + _check_required(messages, 'table') + + return self._fetch_header(self, _extract_messages(self, messages)) +end + +function Mailbox.fetch_body(self, messages) + _check_required(messages, 'table') + + return self._fetch_body(self, _extract_messages(self, messages)) +end + +function Mailbox.fetch_message(self, messages) + _check_required(messages, 'table') + + return self._fetch_message(self, _extract_messages(self, messages)) +end + +function Mailbox.fetch_fields(self, fields, messages) + _check_required(fields, 'table') + _check_required(messages, 'table') + + return self._fetch_fields(self, fields, _extract_messages(self, messages)) +end + +function Mailbox.fetch_structure(self, messages) + _check_required(messages, 'table') + + return self._fetch_structure(self, _extract_messages(self, messages)) +end + +function Mailbox.fetch_parts(self, parts, message) + _check_required(parts, 'table') + _check_required(message, 'number') + + return self._fetch_parts(self, parts, message) +end + + +function Mailbox.is_answered(self) + return self.send_query(self, 'ANSWERED') +end + +function Mailbox.is_deleted(self) + return self.send_query(self, 'DELETED') +end + +function Mailbox.is_draft(self) + return self.send_query(self, 'DRAFT') +end + +function Mailbox.is_flagged(self) + return self.send_query(self, 'FLAGGED') +end + +function Mailbox.is_new(self) + return self.send_query(self, 'NEW') +end + +function Mailbox.is_old(self) + return self.send_query(self, 'OLD') +end + +function Mailbox.is_recent(self) + return self.send_query(self, 'RECENT') +end + +function Mailbox.is_seen(self) + return self.send_query(self, 'SEEN') +end + +function Mailbox.is_unanswered(self) + return self.send_query(self, 'UNANSWERED') +end + +function Mailbox.is_undeleted(self) + return self.send_query(self, 'UNDELETED') +end + +function Mailbox.is_undraft(self) + return self.send_query(self, 'UNDRAFT') +end + +function Mailbox.is_unflagged(self) + return self.send_query(self, 'UNFLAGGED') +end + +function Mailbox.is_unseen(self) + return self.send_query(self, 'UNSEEN') +end + +function Mailbox.is_larger(self, size) + _check_required(size, 'number') + + return self.send_query(self, 'LARGER ' .. tostring(size)) +end + +function Mailbox.is_smaller(self, size) + _check_required(size, 'number') + + return self.send_query(self, 'SMALLER ' .. tostring(size)) +end + + +function Mailbox.arrived_on(self, date) + _check_required(date, 'string') + + return self.send_query(self, 'ON ' .. date) +end + +function Mailbox.arrived_before(self, date) + _check_required(date, 'string') + + return self.send_query(self, 'BEFORE ' .. date) +end + +function Mailbox.arrived_since(self, date) + _check_required(date, 'string') + + return self.send_query(self, 'SINCE ' .. date) +end + +function Mailbox.sent_on(self, date) + _check_required(date, 'string') + + return self.send_query(self, 'SENTON ' .. date) +end + +function Mailbox.sent_before(self, date) + _check_required(date, 'string') + + return self.send_query(self, 'SENTBEFORE ' .. date) +end + +function Mailbox.sent_since(self, date) + _check_required(date, 'string') + + return self.send_query(self, 'SENTSINCE ' .. date) +end + +function Mailbox.is_newer(self, days) + _check_required(days, 'number') + + return self.send_query(self, 'SINCE ' .. form_date(days)) +end + +function Mailbox.is_older(self, days) + _check_required(days, 'number') + + return self.send_query(self, 'BEFORE ' .. form_date(days)) +end + + +function Mailbox.has_flag(self, flag) + _check_required(flag, 'string') + + return self.send_query(self, 'KEYWORD "' .. flag .. '"') +end + + +function Mailbox.contain_field(self, field, string) + _check_required(field, 'string') + _check_required(string, 'string') + + return self.send_query(self, 'HEADER ' .. field .. ' "' .. string .. '"') +end + +function Mailbox.contain_bcc(self, string) + _check_required(string, 'string') + + return self.send_query(self, 'BCC "' .. string .. '"') +end + +function Mailbox.contain_cc(self, string) + _check_required(string, 'string') + + return self.send_query(self, 'CC "' .. string .. '"') +end + +function Mailbox.contain_from(self, string) + _check_required(string, 'string') + + return self.send_query(self, 'FROM "' .. string .. '"') +end + +function Mailbox.contain_subject(self, string) + _check_required(string, 'string') + + return self.send_query(self, 'SUBJECT "' .. string .. '"') +end + +function Mailbox.contain_to(self, string) + _check_required(string, 'string') + + return self.send_query(self, 'TO "' .. string .. '"') +end + +function Mailbox.contain_header(self, string) + _check_required(string, 'string') + + return self.send_query(self, 'TEXT "' .. string .. '" NOT BODY "' .. + string .. '"') +end + +function Mailbox.contain_body(self, string) + _check_required(string, 'string') + + return self.send_query(self, 'BODY "' .. string .. '"') +end + +function Mailbox.contain_message(self, string) + _check_required(string, 'string') + + return self.send_query(self, 'TEXT "' .. string .. '"') +end + + +function Mailbox.match_field(self, field, pattern, messages) + _check_required(field, 'string') + _check_required(pattern, 'string') + + if not messages then + messages = self._send_query(self) + end + local mesgs = _extract_messages(self, messages) + local fields = self._fetch_fields(self, { field }, mesgs) + + if (#mesgs == 0 or fields == nil) then + return Set({}) + end + + local results = {} + for m, f in pairs(fields) do + if (regex_search(pattern, f)) then + table.insert(results, {self, m}) + end + end + + return Set(results) +end + +function Mailbox.match_bcc(self, pattern, messages) + _check_required(pattern, 'string') + + return self.match_field(self, 'Bcc', pattern, messages) +end + +function Mailbox.match_cc(self, pattern, messages) + _check_required(pattern, 'string') + + return self.match_field(self, 'Cc', pattern, messages) +end + +function Mailbox.match_from(self, pattern, messages) + _check_required(pattern, 'string') + + return self.match_field(self, 'From', pattern, messages) +end + +function Mailbox.match_subject(self, pattern, messages) + _check_required(pattern, 'string') + + return self.match_field(self, 'Subject', pattern, messages) +end + +function Mailbox.match_to(self, pattern, messages) + _check_required(pattern, 'string') + + return self.match_field(self, 'To', pattern, messages) +end + +function Mailbox.match_header(self, pattern, messages) + _check_required(pattern, 'string') + + if not messages then + messages = self._send_query(self) + end + local mesgs = _extract_messages(self, messages) + local header = self._fetch_header(self, mesgs) + + if (#mesgs == 0 or header == nil) then + return Set({}) + end + + local results = {} + for m, h in pairs(header) do + if (regex_search(pattern, h)) then + table.insert(results, {self, m}) + end + end + + return Set(results) +end + +function Mailbox.match_body(self, pattern, messages) + _check_required(pattern, 'string') + + if not messages then + messages = self._send_query(self) + end + local mesgs = _extract_messages(self, messages) + local body = self._fetch_body(self, mesgs) + + if (#mesgs == 0 or body == nil) then + return Set({}) + end + + local results = {} + for m, b in pairs(body) do + if (regex_search(pattern, b)) then + table.insert(results, {self, m}) + end + end + + return Set(results) +end + +function Mailbox.match_message(self, pattern, messages) + _check_required(pattern, 'string') + + if not messages then + messages = self._send_query(self) + end + local mesgs = _extract_messages(self, messages) + local full = self._fetch_message(self, mesgs) + + if (#mesgs == 0 or full == nil) then + return Set({}) + end + + local results = {} + for m, f in pairs(full) do + if (regex_search(pattern, f)) then + table.insert(results, {self, m}) + end + end + + return Set(results) +end + +function Mailbox.enter_idle(self) + if (self._account._login_user(self._account) ~= true) then + return false + end + + if (self._cached_select(self, self._account._imap, self._mailbox) ~= + true) then + return false + end + + return ifcore.idle(self._account._imap) +end + +Mailbox._mt.__index = function () end +Mailbox._mt.__newindex = function () end diff --git a/memory.c b/memory.c new file mode 100644 index 0000000..68dfb4b --- /dev/null +++ b/memory.c @@ -0,0 +1,92 @@ +#include +#include +#include +#include + +#include "imapfilter.h" + + +/* + * A malloc() that checks the results and dies in case of error. + */ +void * +xmalloc(size_t size) +{ + void *ptr; + + ptr = (void *)malloc(size); + + if (ptr == NULL) + fatal(ERROR_MEMALLOC, + "allocating memory; %s\n", strerror(errno)); + + return ptr; +} + + +/* + * A realloc() that checks the results and dies in case of error. + */ +void * +xrealloc(void *ptr, size_t size) +{ + + ptr = (void *)realloc(ptr, size); + + if (ptr == NULL) + fatal(ERROR_MEMALLOC, + "allocating memory; %s\n", strerror(errno)); + + return ptr; +} + + +/* + * A free() that dies if fed with NULL pointer. + */ +void +xfree(void *ptr) +{ + + if (ptr == NULL) + fatal(ERROR_MEMALLOC, + "NULL pointer given as argument\n"); + free(ptr); +} + + +/* + * A strdup() that checks the results and dies in case of error. + */ +char * +xstrdup(const char *str) +{ + char *dup; + + dup = strdup(str); + + if (dup == NULL) + fatal(ERROR_MEMALLOC, "allocating memory; %s\n", + strerror(errno)); + + return dup; +} + + +/* + * A strndup() implementation that also checks the results and dies in case of + * error. + */ +char * +xstrndup(const char *str, size_t len) +{ + char *dup; + + dup = (char *)xmalloc((len + 1) * sizeof(char)); + + memcpy(dup, str, len); + + dup[len] = '\0'; + + return dup; +} diff --git a/message.lua b/message.lua new file mode 100644 index 0000000..83ffe5a --- /dev/null +++ b/message.lua @@ -0,0 +1,93 @@ +-- The Message class that represents messages inside a mailbox. + +Message = {} + +Message._mt = {} +setmetatable(Message, Message._mt) + + +Message._mt.__call = function (self, account, mailbox, uid) + local object = {} + + object._type = 'message' + + for key, value in pairs(Message) do + if (type(value) == 'function') then + object[key] = value + end + end + + object._mt = {} + setmetatable(object, object._mt) + + object._account = account + object._mailbox = mailbox + + object._uid = uid + + object._structure = nil + object._header = nil + object._body = nil + object._fields = {} + object._parts = {} + object._size = nil + object._date = nil + + return object +end + + +function Message.fetch_structure(self) + local r = self._mailbox._fetch_structure(self._mailbox, { self._uid }) + return r[self._uid] +end + +function Message.fetch_header(self) + local r = self._mailbox._fetch_header(self._mailbox, { self._uid }) + return r[self._uid] +end + +function Message.fetch_body(self) + local r = self._mailbox._fetch_body(self._mailbox, { self._uid }) + return r[self._uid] +end + +function Message.fetch_message(self) + local r = self._mailbox._fetch_message(self._mailbox, { self._uid }) + return r[self._uid] +end + +function Message.fetch_field(self, field) + local r = self._mailbox._fetch_fields(self._mailbox, { field }, + { self._uid }) + return r[self._uid] +end + +function Message.fetch_fields(self, fields) + local r = self._mailbox._fetch_fields(self._mailbox, fields, { self._uid }) + return r[self._uid] +end + +function Message.fetch_part(self, part) + local r = self._mailbox._fetch_parts(self._mailbox, { part }, self._uid) + return r[part] +end + +function Message.fetch_size(self) + local r = self._mailbox._fetch_size(self._mailbox, { self._uid }) + return r[self._uid] +end + +function Message.fetch_date(self) + local r = self._mailbox._fetch_date(self._mailbox, { self._uid }) + return r[self._uid] +end + +function Message.fetch_flags(self) + local r = self._mailbox._fetch_flags(self._mailbox, { self._uid }) + return r[self._uid] +end + + +Message._mt.__index = function () end +Message._mt.__newindex = function () end diff --git a/misc.c b/misc.c new file mode 100644 index 0000000..d4d125a --- /dev/null +++ b/misc.c @@ -0,0 +1,74 @@ +#include +#include +#include + +#include "imapfilter.h" + + +/* + * An implementation of strstr() with case-insensitivity. + */ +const char * +xstrcasestr(const char *haystack, const char *needle) +{ + const char *h, *n, *c; + size_t hl, nl; + + c = haystack; + n = needle; + hl = strlen(haystack); + nl = strlen(needle); + + while (hl >= nl) { + while (tolower((int)(*c)) != tolower((int)(*needle))) { + c++; + hl--; + if (hl < nl) + return NULL; + } + + h = c; + n = needle; + + while (tolower((int)(*h)) == tolower((int)(*n))) { + h++; + n++; + + if (*n == '\0') + return c; + } + c++; + hl--; + } + + return NULL; +} + + +/* + * Copies at most size characters from the string pointed by src to the array + * pointed by dest, always NULL terminating (unless size == 0). Returns + * pointer to dest. + */ +char * +xstrncpy(char *dst, const char *src, size_t len) +{ + char *d; + const char *s; + size_t n; + + d = dst; + s = src; + n = len; + + while (n != 0) { + if ((*d++ = *s++) == '\0') + break; + n--; + } + + if (n == 0 && len != 0) + *d = '\0'; + + return dst; +} diff --git a/namespace.c b/namespace.c new file mode 100644 index 0000000..6f0e0b5 --- /dev/null +++ b/namespace.c @@ -0,0 +1,78 @@ +#include +#include + +#include "imapfilter.h" +#include "buffer.h" + + +buffer nbuf; /* Namespace buffer. */ + + +/* + * Convert the names of personal mailboxes, using the namespace specified + * by the mail server, from internal to mail server format. + */ +const char * +apply_namespace(const char *mbox, char *prefix, char delim) +{ + int n; + char *c; + + if ((prefix == NULL && delim == '\0') || + (prefix == NULL && delim == '/') || + !strcasecmp(mbox, "INBOX")) + return mbox; + + buffer_reset(&nbuf); + + n = snprintf(nbuf.data, nbuf.size + 1, "%s%s", (prefix ? prefix : ""), + mbox); + if (n > (int)nbuf.size) { + buffer_check(&nbuf, n); + snprintf(nbuf.data, nbuf.size + 1, "%s%s", + (prefix ? prefix : ""), mbox); + } + c = nbuf.data; + while ((c = strchr(c, '/'))) + *(c++) = delim; + + debug("namespace: '%s' -> '%s'\n", mbox, nbuf.data); + + return nbuf.data; +} + + +/* + * Convert the names of personal mailboxes, using the namespace specified by + * the mail server, from mail server format to internal format. + */ +const char * +reverse_namespace(const char *mbox, char *prefix, char delim) +{ + int n, o; + char *c; + + if ((prefix == NULL && delim == '\0') || + (prefix == NULL && delim == '/') || + !strcasecmp(mbox, "INBOX")) + return mbox; + + buffer_reset(&nbuf); + + o = strlen(prefix ? prefix : ""); + if (strncasecmp(mbox, (prefix ? prefix : ""), o)) + o = 0; + + n = snprintf(nbuf.data, nbuf.size + 1, "%s", mbox + o); + if (n > (int)nbuf.size) { + buffer_check(&nbuf, n); + snprintf(nbuf.data, nbuf.size + 1, "%s", mbox + o); + } + c = nbuf.data; + while ((c = strchr(c, delim))) + *(c++) = '/'; + + debug("namespace: '%s' <- '%s'\n", mbox, nbuf.data); + + return nbuf.data; +} diff --git a/options.lua b/options.lua new file mode 100644 index 0000000..01e6314 --- /dev/null +++ b/options.lua @@ -0,0 +1,5 @@ +-- Options related to the interface implementation. + +options.cache = true +options.close = false +options.info = true diff --git a/pathnames.h b/pathnames.h new file mode 100644 index 0000000..550d5b4 --- /dev/null +++ b/pathnames.h @@ -0,0 +1,45 @@ +#ifndef PATHNAMES_H +#define PATHNAMES_H + + +/* Program's home directory. */ +#define PATHNAME_HOME ".imapfilter" + +/* Program's configuration file. */ +#define PATHNAME_CONFIG PATHNAME_HOME "/config.lua" + +/* Lua imapfilter set functions file. */ +#define PATHNAME_COMMON MAKEFILE_SHAREDIR "/common.lua" + +/* Lua imapfilter set functions file. */ +#define PATHNAME_SET MAKEFILE_SHAREDIR "/set.lua" + +/* Lua imapfilter account functions file. */ +#define PATHNAME_ACCOUNT MAKEFILE_SHAREDIR "/account.lua" + +/* Lua imapfilter mailbox functions file. */ +#define PATHNAME_MAILBOX MAKEFILE_SHAREDIR "/mailbox.lua" + +/* Lua imapfilter message functions file. */ +#define PATHNAME_MESSAGE MAKEFILE_SHAREDIR "/message.lua" + +/* Lua imapfilter message functions file. */ +#define PATHNAME_OPTIONS MAKEFILE_SHAREDIR "/options.lua" + +/* Lua imapfilter regex functions file. */ +#define PATHNAME_REGEX MAKEFILE_SHAREDIR "/regex.lua" + +/* Lua imapfilter auxiliary functions file. */ +#define PATHNAME_AUXILIARY MAKEFILE_SHAREDIR "/auxiliary.lua" + +/* Lua imapfilter old interface functions file. */ +#define PATHNAME_DEPRECATED MAKEFILE_SHAREDIR "/deprecated.lua" + +/* SSL/TLS certificates file. */ +#define PATHNAME_CERTS PATHNAME_HOME "/certificates" + +/* Debug temporary file template. */ +#define PATHNAME_DEBUG PATHNAME_HOME "/debug.XXXXXX" + + +#endif /* PATHNAMES_H */ diff --git a/pcre.c b/pcre.c new file mode 100644 index 0000000..cd263ee --- /dev/null +++ b/pcre.c @@ -0,0 +1,268 @@ +#include + +#include +#include +#include + +#include + +#include "imapfilter.h" + + +static int ifre_flags(lua_State *lua); +static int ifre_compile(lua_State *lua); +static int ifre_exec(lua_State *lua); +static int ifre_free(lua_State *lua); + +/* Lua imapfilter library of PCRE related functions. */ +static const luaL_reg ifrelib[] = { + { "flags", ifre_flags }, + { "compile", ifre_compile }, + { "exec", ifre_exec }, + { "free", ifre_free }, + { NULL, NULL } +}; + + +/* + * Return PCRE available compile and exec flags. + */ +static int +ifre_flags(lua_State *lua) +{ + + if (lua_gettop(lua) != 0) + luaL_error(lua, "wrong number of arguments"); + + lua_newtable(lua); + +#ifdef PCRE_CASELESS + set_table_number("CASELESS", PCRE_CASELESS); +#endif +#ifdef PCRE_MULTILINE + set_table_number("MULTILINE", PCRE_MULTILINE); +#endif +#ifdef PCRE_DOTALL + set_table_number("DOTALL", PCRE_DOTALL); +#endif +#ifdef PCRE_EXTENDED + set_table_number("EXTENDED", PCRE_EXTENDED); +#endif +#ifdef PCRE_ANCHORED + set_table_number("ANCHORED", PCRE_ANCHORED); +#endif +#ifdef PCRE_DOLLAR_ENDONLY + set_table_number("DOLLAR_ENDONLY", PCRE_DOLLAR_ENDONLY); +#endif +#ifdef PCRE_EXTRA + set_table_number("EXTRA", PCRE_EXTRA); +#endif +#ifdef PCRE_NOTBOL + set_table_number("NOTBOL", PCRE_NOTBOL); +#endif +#ifdef PCRE_NOTEOL + set_table_number("NOTEOL", PCRE_NOTEOL); +#endif +#ifdef PCRE_UNGREEDY + set_table_number("UNGREEDY", PCRE_UNGREEDY); +#endif +#ifdef PCRE_NOTEMPTY + set_table_number("NOTEMPTY", PCRE_NOTEMPTY); +#endif +#ifdef PCRE_UTF8 + set_table_number("UTF8", PCRE_UTF8); +#endif +#ifdef PCRE_NO_AUTO_CAPTURE + set_table_number("NO_AUTO_CAPTURE", PCRE_NO_AUTO_CAPTURE); +#endif +#ifdef PCRE_NO_UTF8_CHECK + set_table_number("NO_UTF8_CHECK", PCRE_NO_UTF8_CHECK); +#endif +#ifdef PCRE_FIRSTLINE + set_table_number("FIRSTLINE", PCRE_FIRSTLINE); +#endif +#ifdef PCRE_AUTO_CALLOUT + set_table_number("AUTO_CALLOUT", PCRE_AUTO_CALLOUT); +#endif +#ifdef PCRE_PARTIAL + set_table_number("PARTIAL", PCRE_PARTIAL); +#endif +#ifdef PCRE_DFA_SHORTEST + set_table_number("DFA_SHORTEST", PCRE_DFA_SHORTEST); +#endif +#ifdef PCRE_DFA_RESTART + set_table_number("DFA_RESTART", PCRE_DFA_RESTART); +#endif +#ifdef PCRE_FIRSTLINE + set_table_number("FIRSTLINE", PCRE_FIRSTLINE); +#endif +#ifdef PCRE_DUPNAMES + set_table_number("DUPNAMES", PCRE_DUPNAMES); +#endif +#ifdef PCRE_NEWLINE_CR + set_table_number("NEWLINE_CR)", PCRE_NEWLINE_CR); +#endif +#ifdef PCRE_NEWLINE_LF + set_table_number("NEWLINE_LF", PCRE_NEWLINE_LF); +#endif +#ifdef PCRE_NEWLINE_CRLF + set_table_number("NEWLINE_CRLF", PCRE_NEWLINE_CRLF); +#endif +#ifdef PCRE_NEWLINE_ANY + set_table_number("NEWLINE_ANY", PCRE_NEWLINE_ANY); +#endif +#ifdef PCRE_NEWLINE_ANYCRLF + set_table_number("NEWLINE_ANYCRLF", PCRE_NEWLINE_ANYCRLF); +#endif +#ifdef PCRE_BSR_ANYCRLF + set_table_number("PCRE_BSR_ANYCRLF", PCRE_BSR_ANYCRLF); +#endif +#ifdef PCRE_BSR_UNICODE + set_table_number("PCRE_BSR_UNICODE", PCRE_BSR_UNICODE); +#endif +#ifdef PCRE_JAVASCRIPT_COMPAT + set_table_number("PCRE_JAVASCRIPT_COMPAT", PCRE_JAVASCRIPT_COMPAT); +#endif +#ifdef PCRE_NO_START_OPTIMIZE + set_table_number("PCRE_NO_START_OPTIMIZE", PCRE_NO_START_OPTIMIZE); +#endif +#ifdef PCRE_NO_START_OPTIMISE + set_table_number("PCRE_NO_START_OPTIMISE", PCRE_NO_START_OPTIMISE); +#endif +#ifdef PCRE_PARTIAL_HARD + set_table_number("PCRE_PARTIAL_HARD", PCRE_PARTIAL_HARD); +#endif +#ifdef PCRE_NOTEMPTY_ATSTART + set_table_number("PCRE_NOTEMPTY_ATSTART", PCRE_NOTEMPTY_ATSTART); +#endif +#ifdef PCRE_UCP + set_table_number("PCRE_UCP", PCRE_UCP); +#endif + + return 1; +} + + +/* + * Lua implementation of the PCRE compile function. + */ +static int +ifre_compile(lua_State *lua) +{ + pcre **re; + const char *error; + int erroffset; + + if (lua_gettop(lua) != 2) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TSTRING); + luaL_checktype(lua, 2, LUA_TNUMBER); + + re = (pcre **)(lua_newuserdata(lua, sizeof(pcre *))); + + *re = pcre_compile(lua_tostring(lua, 1), lua_tonumber(lua, 2), &error, + &erroffset, NULL); + + if (*re == NULL) { + fprintf(stderr, "RE failed at offset %d: %s\n", erroffset, + error); + lua_pop(lua, 1); + } + + lua_remove(lua, 1); + lua_remove(lua, 1); + + lua_pushboolean(lua, (*re != NULL)); + lua_insert(lua, 1); + + return (*re != NULL ? 2 : 1); +} + + +/* + * Lua implementation of the PCRE exec function. + */ +static int +ifre_exec(lua_State *lua) +{ + int i, n; + pcre *re; + int ovecsize; + int *ovector; + + if (lua_gettop(lua) != 3) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TUSERDATA); + luaL_checktype(lua, 2, LUA_TSTRING); + luaL_checktype(lua, 3, LUA_TNUMBER); + + re = *(pcre **)(lua_touserdata(lua, 1)); + + pcre_fullinfo(re, NULL, PCRE_INFO_CAPTURECOUNT, &ovecsize); + + ovector = (int *)xmalloc(sizeof(int) * (ovecsize + 1) * 3); + + for (i = 0; i <= ovecsize; i++) + ovector[2 * i] = ovector[2 * i + 1] = -1; + + n = pcre_exec(re, NULL, lua_tostring(lua, 2), lua_strlen(lua, 2), 0, + lua_tonumber(lua, 3), ovector, (ovecsize + 1) * 3); + + if (n > 0) + for (i = 1; i < n; i++) + if (ovector[2 * i] != -1 && ovector[2 * i + 1] != -1) + lua_pushlstring(lua, lua_tostring(lua, 2) + + ovector[2 * i], ovector[2 * i + 1] - + ovector[2 * i]); + else + lua_pushnil(lua); + + xfree(ovector); + + lua_remove(lua, 1); + lua_remove(lua, 1); + lua_remove(lua, 1); + + lua_pushboolean(lua, (n > 0)); + lua_insert(lua, 1); + + return (n > 0 ? n : 1); +} + + +/* + * Lua implementation of a PCRE free function. + */ +static int +ifre_free(lua_State *lua) +{ + pcre *re; + + if (lua_gettop(lua) != 1) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TUSERDATA); + + re = *(pcre **)(lua_touserdata(lua, 1)); + + xfree(re); + + lua_remove(lua, 1); + + return 0; +} + + +/* + * Open imapfilter library of PCRE related functions. + */ +LUALIB_API int +luaopen_ifre(lua_State *lua) +{ + + luaL_register(lua, "ifre", ifrelib); + + return 1; +} diff --git a/regex.lua b/regex.lua new file mode 100644 index 0000000..859a7b8 --- /dev/null +++ b/regex.lua @@ -0,0 +1,53 @@ +-- A simple wrapper for PCRE that uses a cache for compiled expressions. + +_regex_cache = {} + +_regex_cache.mt = {} +setmetatable(_regex_cache, _regex_cache.mt) + + +_regex_cache.mt.__index = function (self, key) + local _, _, pattern, cflags = string.find(key, '^(.*)%z(.*)$') + + local _, compiled = ifre.compile(pattern, tonumber(cflags)) + + self[key] = compiled + + return compiled +end + + +function regex_search(pattern, subject, cflags, eflags) + _check_required(pattern, 'string') + _check_required(subject, 'string') + _check_optional(cflags, 'table') + _check_optional(eflags, 'table') + + if (cflags == nil) then + cflags = {} + end + + if (eflags == nil) then + eflags = {} + end + + local flags = ifre.flags() + + cf = 0 + for _, f in ipairs(cflags) do + cf = cf + flags[f] + end + + ef = 0 + for _, f in pairs(eflags) do + ef = cf + flags[f] + end + + local compiled = _regex_cache[pattern .. '\0' .. cf] + + if (compiled == nil) then + return nil + end + + return ifre.exec(compiled, subject, ef) +end diff --git a/regexp.c b/regexp.c new file mode 100644 index 0000000..f107f56 --- /dev/null +++ b/regexp.c @@ -0,0 +1,42 @@ +#include +#include +#include + +#include "imapfilter.h" +#include "regexp.h" + + +/* + * Compile all the patterns and allocate the necessary space for the substring + * matching. + */ +void +regexp_compile(regexp *reg) +{ + regexp *re; + + for (re = reg; re->pattern != NULL; re++) { + re->preg = (regex_t *)xmalloc(sizeof(regex_t)); + regcomp(re->preg, re->pattern, REG_EXTENDED | REG_ICASE); + re->nmatch = re->preg->re_nsub + 1; + re->pmatch = (regmatch_t *)xmalloc(sizeof(regmatch_t) * + re->nmatch); + } +} + + +/* + * Free the compiled regular expressions and the space allocated for the + * substring matching. + */ +void +regexp_free(regexp *reg) +{ + regexp *re; + + for (re = reg; re->pattern != NULL; re++) { + regfree(re->preg); + xfree(re->preg); + xfree(re->pmatch); + } +} diff --git a/regexp.h b/regexp.h new file mode 100644 index 0000000..68e664e --- /dev/null +++ b/regexp.h @@ -0,0 +1,24 @@ +#ifndef REGEXP_H +#define REGEXP_H + + +#include +#include +#include + + +/* Regular expression convenience structure. */ +typedef struct regexp { + const char *pattern; /* Regular expression pattern string. */ + regex_t *preg; /* Compiled regular expression. */ + size_t nmatch; /* Number of subexpressions in pattern. */ + regmatch_t *pmatch; /* Structure for substrings that matched. */ +} regexp; + + +/* regexp.c */ +void regexp_compile(regexp *reg); +void regexp_free(regexp *reg); + + +#endif /* REGEXP_H */ diff --git a/request.c b/request.c new file mode 100644 index 0000000..b315f79 --- /dev/null +++ b/request.c @@ -0,0 +1,918 @@ +#include +#include +#include + +#include "imapfilter.h" +#include "session.h" +#include "buffer.h" + + +extern options opts; + + +int create_mailbox(session *ssn, const char *mbox); + + +/* + * Reset any inactivity autologout timer on the server. + */ +int +request_noop(const char *server, const char *port, const char *user) +{ + int r; + session *s; + + if (!(s = session_find(server, port, user))) + return -1; + + if ((r = response_generic(s, imap_noop(s))) == -1) + goto fail; + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +/* + * Connect to the server, login to the IMAP server, get it's capabilities, get + * the namespace of the mailboxes. + */ +int +request_login(const char *server, const char *port, const char *ssl, + const char *user, const char *pass) +{ + int r = -1, rg = -1; + session *s; + + if ((s = session_find(server, port, user))) + return STATUS_RESPONSE_NONE; + + s = session_new(); + + s->server = xstrdup(server); + s->port = xstrdup(port); + s->username = xstrdup(user); + + if (ssl && strncasecmp(ssl, "tls1", 4) && + strncasecmp(ssl, "ssl3", 4) && strncasecmp(ssl, "ssl2", 4)) + ssl = NULL; + + if (open_connection(s, server, port, ssl) == -1) + goto fail; + + if ((rg = response_greeting(s)) == -1) + goto fail; + + if (opts.debug > 0) + if (response_generic(s, imap_noop(s)) == -1) + goto fail; + + if (response_capability(s, imap_capability(s)) == -1) + goto fail; + +#ifndef NO_SSLTLS + if (!ssl && s->capabilities & CAPABILITY_STARTTLS && + get_option_boolean("starttls")) + switch (response_generic(s, imap_starttls(s))) { + case STATUS_RESPONSE_OK: + if (open_secure_connection(s, server, port, "tls1") + == -1) + goto fail; + if (response_capability(s, imap_capability(s)) == -1) + goto fail; + break; + case -1: + goto fail; + break; + } +#endif + + if (rg != STATUS_RESPONSE_PREAUTH) { +#ifndef NO_CRAMMD5 + if (s->capabilities & CAPABILITY_CRAMMD5 && + get_option_boolean("crammd5")) { + if ((r = auth_cram_md5(s, user, pass)) == -1) + goto fail; + } +#endif + if (r != STATUS_RESPONSE_OK && + (r = response_generic(s, imap_login(s, user, pass))) == -1) + goto fail; + + if (r == STATUS_RESPONSE_NO) { + error("username %s or password rejected at %s\n", + user, server); + goto fail; + } + } else { + r = STATUS_RESPONSE_PREAUTH; + } + if (s->capabilities & CAPABILITY_NAMESPACE && + get_option_boolean("namespace")) { + if (response_namespace(s, imap_namespace(s)) == -1) + goto fail; + } + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +/* + * Logout from the IMAP server and disconnect from the server. + */ +int +request_logout(const char *server, const char *port, const char *user) +{ + int r; + session *s; + + if (!(s = session_find(server, port, user))) + return -1; + + r = response_generic(s, imap_logout(s)); + + close_connection(s); + session_destroy(s); + + return r; +} + + +/* + * Get mailbox's status. + */ +int +request_status(const char *server, const char *port, const char *user, + const char *mbox, unsigned int *exists, unsigned int *recent, + unsigned int *unseen, unsigned int *uidnext) +{ + int t, r; + session *s; + const char *m; + + if (!(s = session_find(server, port, user))) + return -1; + + m = apply_namespace(mbox, s->ns.prefix, s->ns.delim); + + if (s->protocol == PROTOCOL_IMAP4REV1) { + t = imap_status(s, m, "MESSAGES RECENT UNSEEN UIDNEXT"); + if ((r = response_status(s, t, exists, recent, unseen, uidnext)) == -1) + goto fail; + } else { + t = imap_examine(s, m); + if ((r = response_examine(s, t, exists, recent)) == -1) + goto fail; + } + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +/* + * Open mailbox in read-write mode. + */ +int +request_select(const char *server, const char *port, const char *user, + const char *mbox) +{ + int r; + session *s; + const char *m; + + if (!(s = session_find(server, port, user))) + return -1; + + m = apply_namespace(mbox, s->ns.prefix, s->ns.delim); + + if ((r = response_select(s, imap_select(s, m))) == -1) + goto fail; + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +/* + * Close examined/selected mailbox. + */ +int +request_close(const char *server, const char *port, const char *user) +{ + int r; + session *s; + + if (!(s = session_find(server, port, user))) + return -1; + + if ((r = response_generic(s, imap_close(s))) == -1) + goto fail; + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +/* + * Remove all messages marked for deletion from selected mailbox. + */ +int +request_expunge(const char *server, const char *port, const char *user) +{ + int r; + session *s; + + if (!(s = session_find(server, port, user))) + return -1; + + if ((r = response_generic(s, imap_expunge(s))) == -1) + goto fail; + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +/* + * List available mailboxes. + */ +int +request_list(const char *server, const char *port, const char *user, + const char *refer, const char *name, char **mboxs, char **folders) +{ + int t, r; + session *s; + const char *n; + + if (!(s = session_find(server, port, user))) + return -1; + + n = apply_namespace(name, s->ns.prefix, s->ns.delim); + + t = imap_list(s, refer, n); + if ((r = response_list(s, t, mboxs, folders)) == -1) + goto fail; + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +/* + * List subscribed mailboxes. + */ +int +request_lsub(const char *server, const char *port, const char *user, + const char *refer, const char *name, char **mboxs, char **folders) +{ + int t, r; + session *s; + const char *n; + + if (!(s = session_find(server, port, user))) + return -1; + + n = apply_namespace(name, s->ns.prefix, s->ns.delim); + + t = imap_lsub(s, refer, n); + if ((r = response_list(s, t, mboxs, folders)) == -1) + goto fail; + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +/* + * Search selected mailbox according to the supplied search criteria. + */ +int +request_search(const char *server, const char *port, const char *user, + const char *criteria, const char *charset, char **mesgs) +{ + int t, r; + session *s; + + if (!(s = session_find(server, port, user))) + return -1; + + t = imap_search(s, charset, criteria); + if ((r = response_search(s, t, mesgs)) == -1) + goto fail; + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +/* + * Fetch the FLAGS, INTERNALDATE and RFC822.SIZE of the messages. + */ +int +request_fetchfast(const char *server, const char *port, const char *user, + const char *mesg, char **flags, char **date, char **size) +{ + int t, r; + session *s; + + if (!(s = session_find(server, port, user))) + return -1; + + t = imap_fetch(s, mesg, "FAST"); + if ((r = response_fetchfast(s, t, flags, date, size)) == -1) + goto fail; + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +/* + * Fetch the FLAGS of the messages. + */ +int +request_fetchflags(const char *server, const char *port, const char *user, + const char *mesg, char **flags) +{ + int t, r; + session *s; + + if (!(s = session_find(server, port, user))) + return -1; + + t = imap_fetch(s, mesg, "FLAGS"); + if ((r = response_fetchflags(s, t, flags)) == -1) + goto fail; + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +/* + * Fetch the INTERNALDATE of the messages. + */ +int +request_fetchdate(const char *server, const char *port, const char *user, + const char *mesg, char **date) +{ + int t, r; + session *s; + + if (!(s = session_find(server, port, user))) + return -1; + + t = imap_fetch(s, mesg, "INTERNALDATE"); + if ((r = response_fetchdate(s, t, date)) == -1) + goto fail; + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} +/* + * Fetch the RFC822.SIZE of the messages. + */ +int +request_fetchsize(const char *server, const char *port, const char *user, + const char *mesg, char **size) +{ + int t, r; + session *s; + + if (!(s = session_find(server, port, user))) + return -1; + + t = imap_fetch(s, mesg, "RFC822.SIZE"); + if ((r = response_fetchsize(s, t, size)) == -1) + goto fail; + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +/* + * Fetch the body structure, ie. BODYSTRUCTURE, of the messages. + */ +int +request_fetchstructure(const char *server, const char *port, const char *user, + const char *mesg, char **structure) +{ + int t, r; + session *s; + + if (!(s = session_find(server, port, user))) + return -1; + + t = imap_fetch(s, mesg, "BODYSTRUCTURE"); + if ((r = response_fetchstructure(s, t, structure)) == -1) + goto fail; + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +/* + * Fetch the header, ie. BODY[HEADER], of the messages. + */ +int +request_fetchheader(const char *server, const char *port, const char *user, + const char *mesg, char **header, size_t *len) +{ + int t, r; + session *s; + + if (!(s = session_find(server, port, user))) + return -1; + + t = imap_fetch(s, mesg, "BODY.PEEK[HEADER]"); + if ((r = response_fetchbody(s, t, header, len)) == -1) + goto fail; + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +/* + * Fetch the text, ie. BODY[TEXT], of the messages. + */ +int +request_fetchtext(const char *server, const char *port, const char *user, + const char *mesg, char **text, size_t *len) +{ + int t, r; + session *s; + + if (!(s = session_find(server, port, user))) + return -1; + + t = imap_fetch(s, mesg, "BODY.PEEK[TEXT]"); + if ((r = response_fetchbody(s, t, text, len)) == -1) + goto fail; + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +/* + * Fetch the specified header fields, ie. BODY[HEADER.FIELDS ()], of + * the messages. + */ +int +request_fetchfields(const char *server, const char *port, const char *user, + const char *mesg, const char *headerfields, char **fields, size_t *len) +{ + int t, r, n; + session *s; + char *f; + + n = strlen("BODY.PEEK[HEADER.FIELDS ()]") + strlen(headerfields) + 1; + f = (char *)xmalloc(n * sizeof(char)); + snprintf(f, n, "%s%s%s", "BODY.PEEK[HEADER.FIELDS (", headerfields, ")]"); + + if (!(s = session_find(server, port, user))) + return -1; + + t = imap_fetch(s, mesg, f); + if ((r = response_fetchbody(s, t, fields, len)) == -1) + goto fail; + + xfree(f); + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +/* + * Fetch the specified message part, ie. BODY[], of the + * messages. + */ +int +request_fetchpart(const char *server, const char *port, const char *user, + const char *mesg, const char *part, char **bodypart, size_t *len) +{ + int t, r, n; + session *s; + char *f; + + n = strlen("BODY.PEEK[]") + strlen(part) + 1; + f = (char *)xmalloc(n * sizeof(char)); + snprintf(f, n, "%s%s%s", "BODY.PEEK[", part, "]"); + + if (!(s = session_find(server, port, user))) + return -1; + + t = imap_fetch(s, mesg, f); + if ((r = response_fetchbody(s, t, bodypart, len)) == -1) + goto fail; + + xfree(f); + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +/* + * Add, remove or replace the specified flags of the messages. + */ +int +request_store(const char *server, const char *port, const char *user, + const char *mesg, const char *mode, const char *flags) +{ + int t, r; + session *s; + + if (!(s = session_find(server, port, user))) + return -1; + + t = imap_store(s, mesg, mode, flags); + if ((r = response_generic(s, t)) == -1) + goto fail; + + if (xstrcasestr(flags, "\\Deleted") && get_option_boolean("expunge")) + if (response_generic(s, imap_expunge(s)) == -1) + goto fail; + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +/* + * Copy the specified messages to another mailbox. + */ +int +request_copy(const char *server, const char *port, const char *user, + const char *mesg, const char *mbox) +{ + int t, r; + session *s; + const char *m; + + if (!(s = session_find(server, port, user))) + return -1; + + m = apply_namespace(mbox, s->ns.prefix, s->ns.delim); + + do { + t = imap_copy(s, mesg, m); + switch (r = response_generic(s, t)) { + case STATUS_RESPONSE_TRYCREATE: + if (create_mailbox(s, mbox) == -1) + goto fail; + break; + case -1: + goto fail; + break; + } + } while (r == STATUS_RESPONSE_TRYCREATE); + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +/* + * Append supplied message to the specified mailbox. + */ +int +request_append(const char *server, const char *port, const char *user, + const char *mbox, const char *mesg, size_t mesglen, const char *flags, + const char *date) +{ + int t, r; + session *s; + const char *m; + + if (!(s = session_find(server, port, user))) + return -1; + + m = apply_namespace(mbox, s->ns.prefix, s->ns.delim); + + do { + if ((t = imap_append(s, m, flags, date, mesglen)) == -1) + goto fail; + if ((r = response_continuation(s)) == -1) + goto fail; + + switch (r) { + case STATUS_RESPONSE_CONTINUE: + if (imap_continuation(s, mesg, mesglen) == -1) + goto fail; + if ((r = response_generic(s, t)) == -1) + goto fail; + break; + case STATUS_RESPONSE_TRYCREATE: + if (create_mailbox(s, mbox) == -1) + goto fail; + break; + case -1: + goto fail; + break; + } + } while (r == STATUS_RESPONSE_TRYCREATE); + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +/* + * Create the specified mailbox. + */ +int +request_create(const char *server, const char *port, const char *user, + const char *mbox) +{ + int r; + session *s; + const char *m; + + if (!(s = session_find(server, port, user))) + return -1; + + m = apply_namespace(mbox, s->ns.prefix, s->ns.delim); + + if ((r = response_generic(s, imap_create(s, m))) == -1) + goto fail; + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +/* + * Delete the specified mailbox. + */ +int +request_delete(const char *server, const char *port, const char *user, + const char *mbox) +{ + int r; + session *s; + const char *m; + + if (!(s = session_find(server, port, user))) + return -1; + + m = apply_namespace(mbox, s->ns.prefix, s->ns.delim); + + if ((r = response_generic(s, imap_delete(s, m))) == -1) + goto fail; + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +/* + * Rename a mailbox. + */ +int +request_rename(const char *server, const char *port, const char *user, + const char *oldmbox, const char *newmbox) +{ + int r; + session *s; + char *o, *n; + + if (!(s = session_find(server, port, user))) + return -1; + + o = xstrdup(apply_namespace(oldmbox, s->ns.prefix, s->ns.delim)); + n = xstrdup(apply_namespace(newmbox, s->ns.prefix, s->ns.delim)); + + r = response_generic(s, imap_rename(s, o, n)); + + xfree(o); + xfree(n); + + if (r == -1) + goto fail; + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +/* + * Subscribe the specified mailbox. + */ +int +request_subscribe(const char *server, const char *port, const char *user, + const char *mbox) +{ + int r; + session *s; + const char *m; + + if (!(s = session_find(server, port, user))) + return -1; + + m = apply_namespace(mbox, s->ns.prefix, s->ns.delim); + + if ((r = response_generic(s, imap_subscribe(s, m))) == -1) + goto fail; + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +/* + * Unsubscribe the specified mailbox. + */ +int +request_unsubscribe(const char *server, const char *port, const char *user, + const char *mbox) +{ + int r; + session *s; + const char *m; + + if (!(s = session_find(server, port, user))) + return -1; + + m = apply_namespace(mbox, s->ns.prefix, s->ns.delim); + + if ((r = response_generic(s, imap_unsubscribe(s, m))) == -1) + goto fail; + + return r; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +int +request_idle(const char *server, const char *port, const char *user) +{ + int t, rg, ri; + session *s; + + if (!(s = session_find(server, port, user))) + return -1; + + + if (!(s->capabilities & CAPABILITY_IDLE)) + return -1; + + do { + ri = 0; + + t = imap_idle(s); + + if ((rg = response_continuation(s)) == -1) + goto fail; + + if (rg == STATUS_RESPONSE_CONTINUE) { + if ((ri = response_idle(s, t)) == -1) + goto fail; + + imap_done(s); + + if ((rg = response_generic(s, t)) == -1) + goto fail; + } + } while (ri == STATUS_RESPONSE_TIMEOUT); + + return rg; +fail: + close_connection(s); + session_destroy(s); + + return -1; +} + + +/* + * Auxiliary function to create a mailbox. + */ +int +create_mailbox(session *ssn, const char *mbox) +{ + int r; + const char *m; + + m = apply_namespace(mbox, ssn->ns.prefix, ssn->ns.delim); + + if ((r = response_generic(ssn, imap_create(ssn, m))) == -1) + return -1; + + if (get_option_boolean("subscribe")) + if (response_generic(ssn, imap_subscribe(ssn, m)) == -1) + return -1; + + return r; +} + diff --git a/response.c b/response.c new file mode 100644 index 0000000..f38c267 --- /dev/null +++ b/response.c @@ -0,0 +1,846 @@ +#include +#include +#include +#include +#include +#include + +#include "imapfilter.h" +#include "session.h" +#include "buffer.h" +#include "regexp.h" + + +extern options opts; + +buffer ibuf; /* Input buffer. */ +enum { /* Server data responses to be parsed; + * regular expressions index. */ + DATA_RESPONSE_TAGGED, + DATA_RESPONSE_CAPABILITY, + DATA_RESPONSE_AUTHENTICATE, + DATA_RESPONSE_NAMESPACE, + DATA_RESPONSE_STATUS, + DATA_RESPONSE_STATUS_MESSAGES, + DATA_RESPONSE_STATUS_RECENT, + DATA_RESPONSE_STATUS_UNSEEN, + DATA_RESPONSE_STATUS_UIDNEXT, + DATA_RESPONSE_EXAMINE_EXISTS, + DATA_RESPONSE_EXAMINE_RECENT, + DATA_RESPONSE_LIST, + DATA_RESPONSE_SEARCH, + DATA_RESPONSE_FETCH, + DATA_RESPONSE_FETCH_FLAGS, + DATA_RESPONSE_FETCH_DATE, + DATA_RESPONSE_FETCH_SIZE, + DATA_RESPONSE_FETCH_STRUCTURE, + DATA_RESPONSE_FETCH_BODY, + DATA_RESPONSE_IDLE, +}; +regexp responses[] = { /* Server data responses to be parsed; + * regular expressions patterns. */ + { "([[:xdigit:]]{4,4}) (OK|NO|BAD) [[:print:]]*\r\n", NULL, 0, NULL }, + { "\\* CAPABILITY ([[:print:]]*)\r\n", NULL, 0, NULL }, + { "\\+ ([[:graph:]]*)\r\n", NULL, 0, NULL }, + { "\\* NAMESPACE (NIL|\\(\\(\"([[:graph:]]*)\" \"([[:print:]])\"\\)" + "[[:print:]]*\\)) (NIL|\\([[:print:]]*\\)) (NIL|\\([[:print:]]*\\))" + "\r\n", NULL, 0, NULL }, + { "\\* STATUS [[:print:]]* \\(([[:alnum:] ]*)\\) *\r\n", NULL, 0, NULL }, + { "MESSAGES ([[:digit:]]+)", NULL, 0, NULL }, + { "RECENT ([[:digit:]]+)", NULL, 0, NULL }, + { "UNSEEN ([[:digit:]]+)", NULL, 0, NULL }, + { "UIDNEXT ([[:digit:]]+)", NULL, 0, NULL }, + { "\\* ([[:digit:]]+) EXISTS\r\n", NULL, 0, NULL }, + { "\\* ([[:digit:]]+) RECENT\r\n", NULL, 0, NULL }, + { "\\* (LIST|LSUB) \\(([[:print:]]*)\\) (\"[[:print:]]\"|NIL) " + "(\"([[:print:]]+)\"|([[:print:]]+)|\\{([[:digit:]]+)\\}\r\n" + "([[:print:]]*))\r\n", NULL, 0, NULL }, + { "\\* SEARCH ?([[:digit:] ]*)\r\n", NULL, 0, NULL }, + { "\\* [[:digit:]]+ FETCH \\(([[:print:]]*)\\)\r\n", NULL, 0, NULL }, + { "FLAGS \\(([[:print:]]*)\\)", NULL, 0, NULL }, + { "INTERNALDATE \"([[:print:]]*)\"", NULL, 0, NULL }, + { "RFC822.SIZE ([[:digit:]]+)", NULL, 0, NULL }, + { "BODYSTRUCTURE (\\([[:print:]]+\\))", NULL, 0, NULL }, + { "\\* [[:digit:]]+ FETCH \\([[:print:]]*BODY\\[[[:print:]]*\\] " + "(\\{([[:digit:]]+)\\}\r\n|\"([[:print:]]*)\")", NULL, 0, NULL }, + { "\\* [[:digit:]]+ (RECENT|EXISTS)\r\n", NULL, 0, NULL }, + { NULL, NULL, 0, NULL } +}; + + +int receive_response(session *ssn, char *buf, long timeout, int timeoutfail); + +int check_tag(char *buf, session *ssn, int tag); +int check_bye(char *buf); +int check_continuation(char *buf); +int check_trycreate(char *buf); + + +/* + * Read data the server sent. + */ +int +receive_response(session *ssn, char *buf, long timeout, int timeoutfail) +{ + ssize_t n; + + if ((n = socket_read(ssn, buf, INPUT_BUF, timeout ? timeout : + (long)(get_option_number("timeout")), timeoutfail)) == -1) + return -1; + + + if (opts.debug > 0) { + int i; + + debug("getting response (%d):\n\n", ssn->socket); + + for (i = 0; i < n; i++) + debugc(buf[i]); + + debug("\n"); + } + + return n; +} + + +/* + * Search for tagged response in the data that the server sent. + */ +int +check_tag(char *buf, session *ssn, int tag) +{ + int r; + char t[4 + 1]; + regexp *re; + + r = STATUS_RESPONSE_NONE; + + snprintf(t, sizeof(t), "%04X", tag); + + re = &responses[DATA_RESPONSE_TAGGED]; + + if (!regexec(re->preg, buf, re->nmatch, re->pmatch, 0)) { + if (!strncasecmp(buf + re->pmatch[1].rm_so, t, + strlen(t))) { + if (!strncasecmp(buf + re->pmatch[2].rm_so, + "OK", strlen("OK"))) + r = STATUS_RESPONSE_OK; + else if (!strncasecmp(buf + re->pmatch[2].rm_so, + "NO", strlen("NO"))) + r = STATUS_RESPONSE_NO; + else if (!strncasecmp(buf + re->pmatch[2].rm_so, + "BAD", strlen("BAD"))) + r = STATUS_RESPONSE_BAD; + } + } + + if (r != STATUS_RESPONSE_NONE) + verbose("S (%d): %s", ssn->socket, buf + re->pmatch[0].rm_so); + + if (r == STATUS_RESPONSE_NO || r == STATUS_RESPONSE_BAD) + error("IMAP (%d): %s", ssn->socket, buf + re->pmatch[0].rm_so); + + return r; +} + + +/* + * Check if server sent a BYE response (connection is closed immediately). + */ +int +check_bye(char *buf) +{ + + if (xstrcasestr(buf, "* BYE") && + !xstrcasestr(buf, " LOGOUT ")) + return 1; + else + return 0; +} + + +/* + * Check if server sent a PREAUTH response (connection already authenticated + * by external means). + */ +int +check_preauth(char *buf) +{ + + if (xstrcasestr(ibuf.data, "* PREAUTH")) + return 1; + else + return 0; +} + + +/* + * Check if the server sent a continuation request. + */ +int +check_continuation(char *buf) +{ + + if ((buf[0] == '+' && buf[1] == ' ') || xstrcasestr(buf, "\r\n+ ")) + return 1; + else + return 0; +} + + +/* + * Check if the server sent a TRYCREATE response. + */ +int +check_trycreate(char *buf) +{ + + if (xstrcasestr(buf, "[TRYCREATE]")) + return 1; + else + return 0; +} + + +/* + * Get server data and make sure there is a tagged response inside them. + */ +int +response_generic(session *ssn, int tag) +{ + int r; + ssize_t n; + + if (tag == -1) + return -1; + + buffer_reset(&ibuf); + + do { + buffer_check(&ibuf, ibuf.len + INPUT_BUF); + if ((n = receive_response(ssn, ibuf.data + ibuf.len, 0, 1)) == -1) + return -1; + ibuf.len += n; + + if (check_bye(ibuf.data)) + return -1; + } while ((r = check_tag(ibuf.data, ssn, tag)) == STATUS_RESPONSE_NONE); + + if (r == STATUS_RESPONSE_NO && + (check_trycreate(ibuf.data) || get_option_boolean("create"))) + return STATUS_RESPONSE_TRYCREATE; + + return r; +} + + +/* + * Get server data and make sure there is a continuation response inside them. + */ +int +response_continuation(session *ssn) +{ + ssize_t n; + + buffer_reset(&ibuf); + + do { + buffer_check(&ibuf, ibuf.len + INPUT_BUF); + if ((n = receive_response(ssn, ibuf.data + ibuf.len, 0, 1)) == -1) + return -1; + ibuf.len += n; + + if (check_bye(ibuf.data)) + return -1; + } while (!check_continuation(ibuf.data)); + + return STATUS_RESPONSE_CONTINUE; +} + + +/* + * Process the greeting that server sends during connection. + */ +int +response_greeting(session *ssn) +{ + + buffer_reset(&ibuf); + + if (receive_response(ssn, ibuf.data, 0, 1) == -1) + return -1; + + verbose("S (%d): %s", ssn->socket, ibuf.data); + + if (check_bye(ibuf.data)) + return -1; + + if (check_preauth(ibuf.data)) + return STATUS_RESPONSE_PREAUTH; + + return STATUS_RESPONSE_NONE; +} + + +/* + * Process the data that server sent due to IMAP CAPABILITY client request. + */ +int +response_capability(session *ssn, int tag) +{ + int r; + char *s; + regexp *re; + + if ((r = response_generic(ssn, tag)) == -1) + return -1; + + ssn->protocol = PROTOCOL_NONE; + + re = &responses[DATA_RESPONSE_CAPABILITY]; + + if (!regexec(re->preg, ibuf.data, re->nmatch, re->pmatch, 0)) { + s = xstrndup(ibuf.data + re->pmatch[1].rm_so, + re->pmatch[1].rm_eo - re->pmatch[1].rm_so); + + if (xstrcasestr(s, "IMAP4rev1")) + ssn->protocol = PROTOCOL_IMAP4REV1; + else if (xstrcasestr(s, "IMAP4")) + ssn->protocol = PROTOCOL_IMAP4; + else { + error("server supports neither the IMAP4rev1 nor the " + "IMAP4 protocol\n"); + return -1; + } + + ssn->capabilities = CAPABILITY_NONE; + + if (xstrcasestr(s, "NAMESPACE")) + ssn->capabilities |= CAPABILITY_NAMESPACE; +#ifndef NO_CRAMMD5 + if (xstrcasestr(s, "AUTH=CRAM-MD5")) + ssn->capabilities |= CAPABILITY_CRAMMD5; +#endif +#ifndef NO_SSLTLS + if (xstrcasestr(s, "STARTTLS")) + ssn->capabilities |= CAPABILITY_STARTTLS; +#endif + if (xstrcasestr(s, "CHILDREN")) + ssn->capabilities |= CAPABILITY_CHILDREN; + + if (xstrcasestr(s, "IDLE")) + ssn->capabilities |= CAPABILITY_IDLE; + + xfree(s); + } + + return r; +} + + +#ifndef NO_CRAMMD5 +/* + * Process the data that server sent due to IMAP AUTHENTICATE client request. + */ +int +response_authenticate(session *ssn, int tag, unsigned char **cont) +{ + int r; + regexp *re; + + re = &responses[DATA_RESPONSE_AUTHENTICATE]; + + if ((r = response_continuation(ssn)) == STATUS_RESPONSE_CONTINUE && + !regexec(re->preg, ibuf.data, re->nmatch, re->pmatch, 0)) + *cont = (unsigned char *)xstrndup(ibuf.data + re->pmatch[1].rm_so, + re->pmatch[1].rm_eo - re->pmatch[1].rm_so); + + return r; +} +#endif + + +/* + * Process the data that server sent due to IMAP NAMESPACE client request. + */ +int +response_namespace(session *ssn, int tag) +{ + int r, n; + regexp *re; + + if ((r = response_generic(ssn, tag)) == -1) + return -1; + + ssn->ns.prefix = NULL; + ssn->ns.delim = '\0'; + + re = &responses[DATA_RESPONSE_NAMESPACE]; + + if (!regexec(re->preg, ibuf.data, re->nmatch, re->pmatch, 0)) { + n = re->pmatch[2].rm_eo - re->pmatch[2].rm_so; + if (n > 0) + ssn->ns.prefix = xstrndup(ibuf.data + + re->pmatch[2].rm_so, n); + ssn->ns.delim = *(ibuf.data + re->pmatch[3].rm_so); + } + debug("namespace (%d): '%s' '%c'\n", ssn->socket, + (ssn->ns.prefix ? ssn->ns.prefix : ""), ssn->ns.delim); + + return r; +} + + +/* + * Process the data that server sent due to IMAP STATUS client request. + */ +int +response_status(session *ssn, int tag, unsigned int *exist, + unsigned int *recent, unsigned int *unseen, unsigned int *uidnext) +{ + int r; + char *s; + regexp *re; + + if ((r = response_generic(ssn, tag)) == -1) + return -1; + + re = &responses[DATA_RESPONSE_STATUS]; + + if (!regexec(re->preg, ibuf.data, re->nmatch, re->pmatch, 0)) { + s = xstrndup(ibuf.data + re->pmatch[1].rm_so, + re->pmatch[1].rm_eo - re->pmatch[1].rm_so); + + re = &responses[DATA_RESPONSE_STATUS_MESSAGES]; + if (!regexec(re->preg, s, re->nmatch, re->pmatch, 0)) + *exist = strtol(s + re->pmatch[1].rm_so, NULL, 10); + + re = &responses[DATA_RESPONSE_STATUS_RECENT]; + if (!regexec(re->preg, s, re->nmatch, re->pmatch, 0)) + *recent = strtol(s + re->pmatch[1].rm_so, NULL, 10); + + re = &responses[DATA_RESPONSE_STATUS_UNSEEN]; + if (!regexec(re->preg, s, re->nmatch, re->pmatch, 0)) + *unseen = strtol(s + re->pmatch[1].rm_so, NULL, 10); + + re = &responses[DATA_RESPONSE_STATUS_UIDNEXT]; + if (!regexec(re->preg, s, re->nmatch, re->pmatch, 0)) + *uidnext = strtol(s + re->pmatch[1].rm_so, NULL, 10); + + xfree(s); + } + + return r; +} + + +/* + * Process the data that server sent due to IMAP EXAMINE client request. + */ +int +response_examine(session *ssn, int tag, unsigned int *exist, + unsigned int *recent) +{ + int r; + regexp *re; + + if ((r = response_generic(ssn, tag)) == -1) + return -1; + + re = &responses[DATA_RESPONSE_EXAMINE_EXISTS]; + if (!regexec(re->preg, ibuf.data, re->nmatch, re->pmatch, 0)) + *exist = strtol(ibuf.data + re->pmatch[1].rm_so, NULL, 10); + + re = &responses[DATA_RESPONSE_EXAMINE_RECENT]; + if (!regexec(re->preg, ibuf.data, re->nmatch, re->pmatch, 0)) + *recent = strtol(ibuf.data + re->pmatch[1].rm_so, NULL, 10); + + return r; +} + + +/* + * Process the data that server sent due to IMAP SELECT client request. + */ +int +response_select(session *ssn, int tag) +{ + int r; + + if ((r = response_generic(ssn, tag)) == -1) + return -1; + + if (xstrcasestr(ibuf.data, "[READ-ONLY]")) + return STATUS_RESPONSE_READONLY; + + return r; +} + + +/* + * Process the data that server sent due to IMAP LIST or IMAP LSUB client + * request. + */ +int +response_list(session *ssn, int tag, char **mboxs, char **folders) +{ + int r, n; + char *b, *a, *s, *m, *f; + const char *v; + regexp *re; + + if ((r = response_generic(ssn, tag)) == -1) + return -1; + + m = *mboxs = (char *)xmalloc((ibuf.len + 1) * sizeof(char)); + f = *folders = (char *)xmalloc((ibuf.len + 1) * sizeof(char)); + *m = *f = '\0'; + + re = &responses[DATA_RESPONSE_LIST]; + + b = ibuf.data; + while (!regexec(re->preg, b, re->nmatch, re->pmatch, 0)) { + a = xstrndup(b + re->pmatch[2].rm_so, + re->pmatch[2].rm_eo - re->pmatch[2].rm_so); + + if (re->pmatch[5].rm_so != -1 && re->pmatch[5].rm_so != -1) + s = xstrndup(b + re->pmatch[5].rm_so, + re->pmatch[5].rm_eo - re->pmatch[5].rm_so); + else if (re->pmatch[6].rm_so != -1 && + re->pmatch[6].rm_so != -1) + s = xstrndup(b + re->pmatch[6].rm_so, + re->pmatch[6].rm_eo - re->pmatch[6].rm_so); + else + s = xstrndup(b + re->pmatch[8].rm_so, strtoul(b + + re->pmatch[7].rm_so, NULL, 10)); + + v = reverse_namespace(s, ssn->ns.prefix, ssn->ns.delim); + n = strlen(v); + + if (!xstrcasestr(a, "\\NoSelect")) { + xstrncpy(m, v, ibuf.len - (m - *mboxs)); + m += n; + xstrncpy(m, "\n", ibuf.len - (m - *mboxs)); + m += strlen("\n"); + } + + if (!xstrcasestr(a, "\\NoInferiors") && + (!(ssn->capabilities & CAPABILITY_CHILDREN) || + ((ssn->capabilities & CAPABILITY_CHILDREN) && + (xstrcasestr(a, "\\HasChildren")) && + !xstrcasestr(a, "\\HasNoChildren")))) { + xstrncpy(f, v, ibuf.len - (f - *folders)); + f += n; + xstrncpy(f, "\n", ibuf.len - (f - *folders)); + f += strlen("\n"); + } + + b += re->pmatch[0].rm_eo; + + xfree(a); + xfree(s); + } + + return r; +} + + +/* + * Process the data that server sent due to IMAP SEARCH client request. + */ +int +response_search(session *ssn, int tag, char **mesgs) +{ + int r; + unsigned int min; + regexp *re; + char *b, *m; + + if ((r = response_generic(ssn, tag)) == -1) + return -1; + + re = &responses[DATA_RESPONSE_SEARCH]; + + b = ibuf.data; + m = NULL; + while (!regexec(re->preg, b, re->nmatch, re->pmatch, 0)) { + if (!*mesgs) { + m = *mesgs = (char *)xmalloc((ibuf.len + 1) * + sizeof(char)); + *m = '\0'; + } + + min = (unsigned int)(re->pmatch[1].rm_eo - re->pmatch[1].rm_so) < ibuf.len ? + (unsigned int)(re->pmatch[1].rm_eo - re->pmatch[1].rm_so) : + ibuf.len; + + xstrncpy(m, b + re->pmatch[1].rm_so, min); + m += min; + xstrncpy(m++, " ", ibuf.len - min); + + b += re->pmatch[0].rm_eo; + } + + return r; +} + + +/* + * Process the data that server sent due to IMAP FETCH FAST client request. + */ +int +response_fetchfast(session *ssn, int tag, char **flags, char **date, + char **size) +{ + int r; + char *s; + regexp *re; + + if ((r = response_generic(ssn, tag)) == -1) + return -1; + + re = &responses[DATA_RESPONSE_FETCH]; + if (!regexec(re->preg, ibuf.data, re->nmatch, re->pmatch, 0)) { + s = xstrndup(ibuf.data + re->pmatch[1].rm_so, + re->pmatch[1].rm_eo - re->pmatch[1].rm_so); + + re = &responses[DATA_RESPONSE_FETCH_FLAGS]; + if (!regexec(re->preg, s, re->nmatch, re->pmatch, 0)) + *flags = xstrndup(s + re->pmatch[1].rm_so, + re->pmatch[1].rm_eo - re->pmatch[1].rm_so); + + re = &responses[DATA_RESPONSE_FETCH_DATE]; + if (!regexec(re->preg, s, re->nmatch, re->pmatch, 0)) + *date = xstrndup(s + re->pmatch[1].rm_so, + re->pmatch[1].rm_eo - re->pmatch[1].rm_so); + + re = &responses[DATA_RESPONSE_FETCH_SIZE]; + if (!regexec(re->preg, s, re->nmatch, re->pmatch, 0)) + *size = xstrndup(s + re->pmatch[1].rm_so, + re->pmatch[1].rm_eo - re->pmatch[1].rm_so); + + xfree(s); + } + + return r; +} + + +/* + * Process the data that server sent due to IMAP FETCH FLAGS client request. + */ +int +response_fetchflags(session *ssn, int tag, char **flags) +{ + int r; + char *s; + regexp *re; + + if ((r = response_generic(ssn, tag)) == -1) + return -1; + + re = &responses[DATA_RESPONSE_FETCH]; + if (!regexec(re->preg, ibuf.data, re->nmatch, re->pmatch, 0)) { + s = xstrndup(ibuf.data + re->pmatch[1].rm_so, + re->pmatch[1].rm_eo - re->pmatch[1].rm_so); + + re = &responses[DATA_RESPONSE_FETCH_FLAGS]; + if (!regexec(re->preg, s, re->nmatch, re->pmatch, 0)) + *flags = xstrndup(s + re->pmatch[1].rm_so, + re->pmatch[1].rm_eo - re->pmatch[1].rm_so); + + xfree(s); + } + + return r; +} + + +/* + * Process the data that server sent due to IMAP FETCH INTERNALDATE client + * request. + */ +int +response_fetchdate(session *ssn, int tag, char **date) +{ + int r; + char *s; + regexp *re; + + if ((r = response_generic(ssn, tag)) == -1) + return -1; + + re = &responses[DATA_RESPONSE_FETCH]; + if (!regexec(re->preg, ibuf.data, re->nmatch, re->pmatch, 0)) { + s = xstrndup(ibuf.data + re->pmatch[1].rm_so, + re->pmatch[1].rm_eo - re->pmatch[1].rm_so); + + re = &responses[DATA_RESPONSE_FETCH_DATE]; + if (!regexec(re->preg, s, re->nmatch, re->pmatch, 0)) + *date = xstrndup(s + re->pmatch[1].rm_so, + re->pmatch[1].rm_eo - re->pmatch[1].rm_so); + + xfree(s); + } + + return r; +} + + +/* + * Process the data that server sent due to IMAP FETCH RFC822.SIZE client + * request. + */ +int +response_fetchsize(session *ssn, int tag, char **size) +{ + int r; + char *s; + regexp *re; + + if ((r = response_generic(ssn, tag)) == -1) + return -1; + + re = &responses[DATA_RESPONSE_FETCH]; + if (!regexec(re->preg, ibuf.data, re->nmatch, re->pmatch, 0)) { + s = xstrndup(ibuf.data + re->pmatch[1].rm_so, + re->pmatch[1].rm_eo - re->pmatch[1].rm_so); + + re = &responses[DATA_RESPONSE_FETCH_SIZE]; + if (!regexec(re->preg, s, re->nmatch, re->pmatch, 0)) + *size = xstrndup(s + re->pmatch[1].rm_so, + re->pmatch[1].rm_eo - re->pmatch[1].rm_so); + + xfree(s); + } + + return r; +} + + +/* + * Process the data that server sent due to IMAP FETCH BODYSTRUCTURE client + * request. + */ +int +response_fetchstructure(session *ssn, int tag, char **structure) +{ + int r; + char *s; + regexp *re; + + if ((r = response_generic(ssn, tag)) == -1) + return -1; + + re = &responses[DATA_RESPONSE_FETCH]; + if (!regexec(re->preg, ibuf.data, re->nmatch, re->pmatch, 0)) { + s = xstrndup(ibuf.data + re->pmatch[1].rm_so, + re->pmatch[1].rm_eo - re->pmatch[1].rm_so); + + re = &responses[DATA_RESPONSE_FETCH_STRUCTURE]; + if (!regexec(re->preg, s, re->nmatch, re->pmatch, 0)) { + *structure = xstrndup(s + re->pmatch[1].rm_so, + re->pmatch[1].rm_eo - re->pmatch[1].rm_so); + } + xfree(s); + } + + return r; +} + + +/* + * Process the data that server sent due to IMAP FETCH BODY[] client request, + * ie. FETCH BODY[HEADER], FETCH BODY[TEXT], FETCH BODY[HEADER.FIELDS + * ()], FETCH BODY[]. + */ +int +response_fetchbody(session *ssn, int tag, char **body, size_t *len) +{ + int r, match; + unsigned int offset; + ssize_t n; + regexp *re; + + if (tag == -1) + return -1; + + buffer_reset(&ibuf); + + match = -1; + offset = 0; + + re = &responses[DATA_RESPONSE_FETCH_BODY]; + + do { + buffer_check(&ibuf, ibuf.len + INPUT_BUF); + if ((n = receive_response(ssn, ibuf.data + ibuf.len, 0, 1)) == -1) + return -1; + ibuf.len += n; + + if (match != 0) { + match = regexec(re->preg, ibuf.data, re->nmatch, + re->pmatch, 0); + + if (match == 0 && re->pmatch[2].rm_so != -1 && + re->pmatch[2].rm_eo != -1) { + *len = strtoul(ibuf.data + re->pmatch[2].rm_so, + NULL, 10); + offset = re->pmatch[0].rm_eo + *len; + } + } + + if (offset != 0 && ibuf.len >= offset) { + if (check_bye(ibuf.data + offset)) + return -1; + } + } while (ibuf.len < offset || (r = check_tag(ibuf.data + offset, ssn, + tag)) == STATUS_RESPONSE_NONE); + + if (match == 0) { + if (re->pmatch[2].rm_so != -1 && + re->pmatch[2].rm_eo != -1) { + *body = ibuf.data + re->pmatch[0].rm_eo; + } else { + *body = ibuf.data + re->pmatch[3].rm_so; + *len = re->pmatch[3].rm_eo - re->pmatch[3].rm_so; + } + } + + return r; +} + + +/* + * Process the data that server sent due to IMAP IDLE client request. + */ +int +response_idle(session *ssn, int tag) +{ + regexp *re; + + re = &responses[DATA_RESPONSE_IDLE]; + + do { + buffer_reset(&ibuf); + + switch (receive_response(ssn, ibuf.data, + get_option_number("keepalive") * 60, 0)) { + case -1: + return -1; + break; /* NOTREACHED */ + case 0: + return STATUS_RESPONSE_TIMEOUT; + break; /* NOTREACHED */ + } + + verbose("S (%d): %s", ssn->socket, ibuf.data); + + if (check_bye(ibuf.data)) + return -1; + + } while (regexec(re->preg, ibuf.data, re->nmatch, re->pmatch, 0)); + + return STATUS_RESPONSE_UNTAGGED; +} diff --git a/sample.config.lua b/sample.config.lua new file mode 100644 index 0000000..a4461c1 --- /dev/null +++ b/sample.config.lua @@ -0,0 +1,129 @@ +--------------- +-- Options -- +--------------- + +options.timeout = 120 +options.subscribe = true + + +---------------- +-- Accounts -- +---------------- + +-- Connects to "imap1.mail.server", as user "user1" with "secret1" as +-- password. +account1 = IMAP { + server = 'imap1.mail.server', + username = 'user1', + password = 'secret1', +} + +-- Another account which connects to the mail server using the SSLv3 +-- protocol. +account2 = IMAP { + server = 'imap2.mail.server', + username = 'user2', + password = 'secret2', + ssl = 'ssl3', +} + +-- Get a list of the available mailboxes and folders +mailboxes, folders = account1:list_all() + +-- Get a list of the subscribed mailboxes and folders +mailboxes, folders = account1:list_subscribed() + +-- Create a mailbox +account1:create_mailbox('Friends') + +-- Subscribe a mailbox +account1:subscribe_mailbox('Friends') + + +----------------- +-- Mailboxes -- +----------------- + +-- Get the status of a mailbox +account1.INBOX:check_status() + +-- Get all the messages in the mailbox. +results = account1.INBOX:select_all() + +-- Get newly arrived, unread messages +results = account1.INBOX:is_new() + +-- Get unseen messages with the specified "From" header. +results = account1.INBOX:is_unseen() * + account1.INBOX:contain_from('weekly-news@news.letter') + +-- Copy messages between mailboxes at the same account. +results:copy_messages(account1.news) + +-- Get messages with the specified "From" header but without the +-- specified "Subject" header. +results = account1.INBOX:contain_from('announce@my.unix.os') - + account1.INBOX:contain_subject('security advisory') + +-- Copy messages between mailboxes at a different account. +results:copy_messages(account2.security) + +-- Get messages with any of the specified headers. +results = account1.INBOX:contain_from('marketing@company.junk') + + account1.INBOX:contain_from('advertising@annoying.promotion') + + account1.INBOX:contain_subject('new great products') + +-- Delete messages. +results:delete_messages() + +-- Get messages with the specified "Sender" header, which are older than +-- 30 days. +results = account1.INBOX:contain_field('sender', 'owner@announce-list') * + account1.INBOX:is_older(30) + +-- Move messages to the "announce" mailbox inside the "lists" folder. +results:move_messages(account1['lists/announce']) + +-- Get messages, in the "devel" mailbox inside the "lists" folder, with the +-- specified "Subject" header and a size less than 50000 octets (bytes). +results = account1['lists/devel']:contain_subject('[patch]') * + account1['lists/devel']:is_smaller(50000) + +-- Move messages to the "patch" mailbox. +results:move_messages(account2.patch) + +-- Get recent, unseen messages, that have either one of the specified +-- "From" headers, but do not have the specified pattern in the body of +-- the message. +results = ( account1.INBOX:is_recent() * + account1.INBOX:is_unseen() * + ( account1.INBOX:contain_from('tux@penguin.land') + + account1.INBOX:contain_from('beastie@daemon.land') ) ) - + account1.INBOX:match_body('.*all.work.and.no.play.*') + +-- Mark messages as important. +results:mark_flagged() + +-- Get all messages in two mailboxes residing in the same server. +results = account1.news:select_all() + + account1.security:select_all() + +-- Mark messages as seen. +results:mark_seen() + +-- Get recent messages in two mailboxes residing in different servers. +results = account1.INBOX:is_recent() + + account2.INBOX:is_recent() + +-- Flag messages as seen and important. +results:add_flags({ '\\Seen', '\\Flagged' }) + +-- Get unseen messages. +results = account1.INBOX:is_unseen() + +-- From the messages that were unseen, match only those with the specified +-- regular expression in the header. +newresults = results:match_header('^.+MailScanner.*Check: [Ss]pam$') + +-- Delete those messages. +newresults:delete_messages() diff --git a/sample.extend.lua b/sample.extend.lua new file mode 100644 index 0000000..9f570f8 --- /dev/null +++ b/sample.extend.lua @@ -0,0 +1,103 @@ +-- +-- This file contains examples on how IMAPFilter can be extended using +-- the Lua programming language. +-- + + +-- IMAPFilter can be detached from the controlling terminal and run in +-- the background as a system daemon. +-- +-- The auxiliary function daemon_mode() is supplied for conveniency. +-- The following example puts imapfilter in the background and runs +-- endlessly, executing the commands in the forever() function and +-- sleeping for 600 seconds between intervals: + +function forever() + results = myaccount.mymailbox:is_old() + results:move_messages(myaccount.archive) +end + +become_daemon(600, forever) + + +-- IMAPFilter can take advantage of all those filtering utilities that +-- are available and use a wide range of heuristic tests, text analysis, +-- internet-based realtime blacklists, advanced learning algorithms, +-- etc. to classify mail. IMAPFilter can pipe a message to a program +-- and act on the message based on the program's exit status. +-- +-- The auxiliary function pipe_to() is supplied for conveniency. For +-- example if there was a utility named "bayesian-spam-filter", which +-- returned 1 when it considered the message "spam" and 0 otherwise: + +all = myaccount.mymailbox:select_all() + +results = Set {} +for _, mesg in ipairs(all) do + mbox, uid = unpack(mesg) + text = mbox[uid]:fetch_message() + if (pipe_to('bayesian-spam-filter', text) == 1) then + table.insert(results, mesg) + end +end + +results:delete_messages() + + +-- One might want to run the bayesian filter only in those parts (attachments) +-- of the message that are of type text/plain and smaller than 1024 bytes. +-- This is possible using the fetch_structure() and fetch_part() functions: + +all = myaccount.mymailbox:select_all() + +results = Set {} +for _, mesg in ipairs(all) do + mbox, uid = unpack(mesg) + structure = mbox[uid]:fetch_structure() + for partid, partinf in pairs(structure) do + if partinf.type:lower() == 'text/plain' and partinf.size < 1024 then + part = mbox[uid]:fetch_part(partid) + if (pipe_to('bayesian-spam-filter', part) == 1) then + table.insert(results, mesg) + break + end + end + end +end + +results:delete_messages() + + +-- Passwords could be extracted during execution time from an encrypted +-- file. +-- +-- The file is encrypted using the openssl(1) command line tool. For +-- example the "passwords.txt" file: +-- +-- secret1 secret2 +-- +-- ... is encrypted and saved to a file named "passwords.enc" with the +-- command: +-- +-- $ openssl bf -salt -in passwords.txt -out passwords.enc +-- +-- The auxiliary function pipe_from() is supplied for conveniency. The +-- user is prompted to enter the decryption password, the file is +-- decrypted and the account passwords are set accordingly: + +status, output = pipe_from('openssl bf -d -salt -in ~/passwords.enc') + +_, _, password1, password2 = string.find(output, '([%w%p]+)\n([%w%p]+)\n') + +account1 = IMAP { + server = 'imap1.mail.server', + username = 'user1', + password = password1 +} + +account2 = IMAP { + server = 'imap2.mail.server', + username = 'user2', + password = password2 +} + diff --git a/session.c b/session.c new file mode 100644 index 0000000..5978268 --- /dev/null +++ b/session.c @@ -0,0 +1,101 @@ +#include +#include + +#include "imapfilter.h" +#include "session.h" +#include "list.h" + + +extern list *sessions; + + +/* + * Allocate memory for a new session and add it to the sessions linked list. + */ +session * +session_new(void) +{ + session *s; + + s = (session *)xmalloc(sizeof(session)); + + session_init(s); + + sessions = list_append(sessions, s); + + return s; +} + + +/* + * Set session variables to safe values. + */ +void +session_init(session *ssn) +{ + + ssn->server = NULL; + ssn->port = NULL; + ssn->username = NULL; + ssn->socket = -1; +#ifndef NO_SSLTLS + ssn->ssl = NULL; +#endif + ssn->protocol = PROTOCOL_NONE; + ssn->capabilities = CAPABILITY_NONE; + ssn->ns.prefix = NULL; + ssn->ns.delim = '\0'; +} + + +/* + * Remove session from sessions linked list. + */ +void +session_destroy(session *ssn) +{ + + sessions = list_remove(sessions, ssn); + + session_free(ssn); +} + + +/* + * Free session allocated memory. + */ +void +session_free(session *ssn) +{ + + if (ssn->server) + xfree(ssn->server); + if (ssn->port) + xfree(ssn->port); + if (ssn->username) + xfree(ssn->username); + if (ssn->ns.prefix) + xfree(ssn->ns.prefix); + xfree(ssn); +} + + +/* + * Based on the specified socket, find an active IMAP session. + */ +session * +session_find(const char *serv, const char *port, const char *user) +{ + list *l; + session *s; + + for (l = sessions; l; l = l->next) { + s = l->data; + if (!strcmp(s->server, serv) && + !strcmp(s->port, port) && + !strcmp(s->username, user)) + return s; + } + + return NULL; +} diff --git a/session.h b/session.h new file mode 100644 index 0000000..1423ce3 --- /dev/null +++ b/session.h @@ -0,0 +1,37 @@ +#ifndef SESSION_H +#define SESSION_H + + +#ifndef NO_SSLTLS +#include +#endif + + +/* IMAP session. */ +typedef struct session { + char *server; /* Server hostname. */ + char *port; /* Server port. */ + char *username; /* User name. */ + int socket; /* Socket. */ +#ifndef NO_SSLTLS + SSL *ssl; /* SSL socket. */ +#endif + unsigned int protocol; /* IMAP protocol. Currently IMAP4rev1 and + * IMAP4 are supported. */ + unsigned int capabilities; /* Capabilities of the mail server. */ + struct { /* Namespace of the mail server's mailboxes. */ + char *prefix; /* Namespace prefix. */ + char delim; /* Namespace delimiter. */ + } ns; +} session; + + +/* session.c */ +session *session_new(void); +void session_init(session *ssn); +void session_destroy(session *ssn); +void session_free(session *ssn); +session *session_find(const char *server, const char *port, const char *user); + + +#endif /* SESSION_H */ diff --git a/set.lua b/set.lua new file mode 100644 index 0000000..6f192dd --- /dev/null +++ b/set.lua @@ -0,0 +1,700 @@ +-- A simple implementation of sets. + +Set = {} + +Set._mt = {} +setmetatable(Set, Set._mt) + + +function Set._new(self, values) + local object + + object = values or {} + + for key, value in pairs(Set) do + if (type(value) == 'function') then + object[key] = value + end + end + + object._type = 'set' + + object._mt = {} + object._mt.__add = object._union + object._mt.__mul = object._intersection + object._mt.__sub = object._difference + setmetatable(object, object._mt) + + return object +end + +function Set._union(seta, setb) + local set = Set() + local t = {} + + for _, v in ipairs(seta) do + b, m = unpack(v) + if not t[b] then t[b] = {} end + t[b][m] = true + end + + for _, v in ipairs(setb) do + b, m = unpack(v) + if not t[b] then t[b] = {} end + t[b][m] = true + end + + for b in pairs(t) do + for m in pairs(t[b]) do + table.insert(set, { b, m }) + end + end + + return set +end + +function Set._intersection(seta, setb) + local set = Set() + local ta = {} + local tb = {} + + for _, v in ipairs(seta) do + b, m = unpack(v) + if not ta[b] then ta[b] = {} end + ta[b][m] = true + end + + for _, v in ipairs(setb) do + b, m = unpack(v) + if not tb[b] then tb[b] = {} end + tb[b][m] = true + end + + for b in pairs(ta) do + if tb[b] then + for m in pairs(ta[b]) do + if tb[b][m] then + table.insert(set, { b, m }) + end + end + end + end + + return set +end + +function Set._difference(seta, setb) + local set = Set() + local t = {} + + for _, v in ipairs(seta) do + b, m = unpack(v) + if not t[b] then t[b] = {} end + t[b][m] = true + end + + for _, v in ipairs(setb) do + b, m = unpack(v) + if t[b] then + t[b][m] = nil + end + end + + for b in pairs(t) do + for m in pairs(t[b]) do + table.insert(set, { b, m }) + end + end + + return set +end + + +function Set.add_flags(self, flags) + _check_required(flags, 'table') + + local r = true + for mbox in pairs(_extract_mailboxes(self)) do + if not mbox.add_flags(mbox, flags, self) then + r = false + end + end + return r +end + +function Set.remove_flags(self, flags) + _check_required(flags, 'table') + + local r = true + for mbox in pairs(_extract_mailboxes(self)) do + if not mbox.remove_flags(mbox, flags, self) then + r = false + end + end + return r +end + +function Set.replace_flags(self, flags) + _check_required(flags, 'table') + + local r = true + for mbox in pairs(_extract_mailboxes(self)) do + if not mbox.replace_flags(mbox, flags, self) then + r = false + end + end + return r +end + +function Set.mark_answered(self) + local r = true + for mbox in pairs(_extract_mailboxes(self)) do + if not mbox.mark_answered(mbox, self) then + r = false + end + end + return r +end + +function Set.mark_deleted(self) + local r = true + for mbox in pairs(_extract_mailboxes(self)) do + if not mbox.mark_deleted(mbox, self) then + r = false + end + end + return r +end + +function Set.mark_draft(self) + local r = true + for mbox in pairs(_extract_mailboxes(self)) do + if not mbox.mark_draft(mbox, self) then + r = false + end + end + return r +end + +function Set.mark_flagged(self) + local r = true + for mbox in pairs(_extract_mailboxes(self)) do + if not mbox.mark_flagged(mbox, self) then + r = false + end + end + return r +end + +function Set.mark_seen(self) + local r = true + for mbox in pairs(_extract_mailboxes(self)) do + if not mbox.mark_seen(mbox, self) then + r = false + end + end + return r +end + +function Set.unmark_answered(self) + local r = true + for mbox in pairs(_extract_mailboxes(self)) do + if not mbox.unmark_answered(mbox, self) then + r = false + end + end + return r +end + +function Set.unmark_deleted(self) + local r = true + for mbox in pairs(_extract_mailboxes(self)) do + if not mbox.unmark_deleted(mbox, self) then + r = false + end + end + return r +end + +function Set.unmark_draft(self) + local r = true + for mbox in pairs(_extract_mailboxes(self)) do + if not mbox.unmark_draft(mbox, self) then + r = false + end + end + return r +end + +function Set.unmark_flagged(self) + local r = true + for mbox in pairs(_extract_mailboxes(self)) do + if not mbox.unmark_flagged(mbox, self) then + r = false + end + end + return r +end + +function Set.unmark_seen(self) + local r = true + for mbox in pairs(_extract_mailboxes(self)) do + if not mbox.unmark_seen(mbox, self) then + r = false + end + end + return r +end + +function Set.delete_messages(self) + local r = true + for mbox in pairs(_extract_mailboxes(self)) do + if not mbox.delete_messages(mbox, self) then + r = false + end + end + return r +end + +function Set.copy_messages(self, dest) + _check_required(dest, 'table') + + local r = true + for mbox in pairs(_extract_mailboxes(self)) do + if not mbox.copy_messages(mbox, dest, self) then + r = false + end + end + return r +end + +function Set.move_messages(self, dest) + _check_required(dest, 'table') + + local r = true + for mbox in pairs(_extract_mailboxes(self)) do + if not mbox.move_messages(mbox, dest, self) then + r = false + end + end + return r +end + + +function Set.select_all(self) + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.select_all(mbox) + end + return self * set +end + +function Set.send_query(self, criteria) + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.send_query(mbox, criteria) + end + return self * set +end + +function Set.is_answered(self) + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.is_answered(mbox) + end + return self * set +end + +function Set.is_deleted(self) + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.is_deleted(mbox) + end + return self * set +end + +function Set.is_draft(self) + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.is_draft(mbox) + end + return self * set +end + +function Set.is_flagged(self) + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.is_flagged(mbox) + end + return self * set +end + +function Set.is_new(self) + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.is_new(mbox) + end + return self * set +end + +function Set.is_old(self) + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.is_old(mbox) + end + return self * set +end + +function Set.is_recent(self) + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.is_recent(mbox) + end + return self * set +end + +function Set.is_seen(self) + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.is_seen(mbox) + end + return self * set +end + +function Set.is_unanswered(self) + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.is_unanswered(mbox) + end + return self * set +end + +function Set.is_undeleted(self) + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.is_undeleted(mbox) + end + return self * set +end + +function Set.is_undraft(self) + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.is_undraft(mbox) + end + return self * set +end + +function Set.is_unflagged(self) + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.is_unflagged(mbox) + end + return self * set +end + +function Set.is_unseen(self) + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.is_unseen(mbox) + end + return self * set +end + +function Set.is_larger(self, size) + _check_required(size, 'number') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.is_larger(mbox, size) + end + return self * set +end + +function Set.is_smaller(self, size) + _check_required(size, 'number') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.is_smaller(mbox, size) + end + return self * set +end + + +function Set.arrived_on(self, date) + _check_required(date, 'string') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.arrived_on(mbox, date) + end + return self * set +end + +function Set.arrived_before(self, date) + _check_required(date, 'string') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.arrived_before(mbox, date) + end + return self * set +end + +function Set.arrived_since(self, date) + _check_required(date, 'string') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.arrived_since(mbox, date) + end + return self * set +end + +function Set.sent_on(self, date) + _check_required(date, 'string') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.sent_on(mbox, date) + end + return self * set +end + +function Set.sent_before(self, date) + _check_required(date, 'string') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.sent_before(mbox, date) + end + return self * set +end + +function Set.sent_since(self, date) + _check_required(date, 'string') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.sent_since(mbox, date) + end + return self * set +end + +function Set.is_newer(self, days) + _check_required(days, 'number') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.is_newer(mbox, days) + end + return self * set +end + +function Set.is_older(self, days) + _check_required(days, 'number') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.is_older(mbox, days) + end + return self * set +end + + +function Set.has_flag(self, flag) + _check_required(flag, 'string') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.has_flag(mbox, flag) + end + return self * set +end + + +function Set.contain_field(self, field, string) + _check_required(field, 'string') + _check_required(string, 'string') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.contain_field(mbox, field, string) + end + return self * set +end + +function Set.contain_bcc(self, string) + _check_required(string, 'string') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.contain_bcc(mbox, string) + end + return self * set +end + +function Set.contain_cc(self, string) + _check_required(string, 'string') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.contain_cc(mbox, string) + end + return self * set +end + +function Set.contain_from(self, string) + _check_required(string, 'string') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.contain_from(mbox, string) + end + return self * set +end + +function Set.contain_subject(self, string) + _check_required(string, 'string') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.contain_subject(mbox, string) + end + return self * set +end + +function Set.contain_to(self, string) + _check_required(string, 'string') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.contain_to(mbox, string) + end + return self * set +end + +function Set.contain_header(self, string) + _check_required(string, 'string') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.contain_header(mbox, string) + end + return self * set +end + +function Set.contain_body(self, string) + _check_required(string, 'string') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.contain_body(mbox, string) + end + return self * set +end + +function Set.contain_message(self, string) + _check_required(string, 'string') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.contain_message(mbox, string) + end + return self * set +end + +function Set.match_bcc(self, pattern) + _check_required(pattern, 'string') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.match_bcc(mbox, pattern, self) + end + return self * set +end + +function Set.match_cc(self, pattern) + _check_required(pattern, 'string') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.match_cc(mbox, pattern, self) + end + return self * set +end + +function Set.match_from(self, pattern) + _check_required(pattern, 'string') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.match_from(mbox, pattern, self) + end + return self * set +end + +function Set.match_subject(self, pattern) + _check_required(pattern, 'string') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.match_subject(mbox, pattern, self) + end + return self * set +end + +function Set.match_to(self, pattern) + _check_required(pattern, 'string') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.match_to(mbox, pattern, self) + end + return self * set +end + +function Set.match_field(self, field, pattern) + _check_required(field, 'string') + _check_required(pattern, 'string') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.match_field(mbox, field, pattern, self) + end + return self * set +end + +function Set.match_header(self, pattern) + _check_required(pattern, 'string') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.match_header(mbox, pattern, self) + end + return self * set +end + +function Set.match_body(self, pattern) + _check_required(pattern, 'string') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.match_body(mbox, pattern, self) + end + return self * set +end + +function Set.match_message(self, pattern) + _check_required(pattern, 'string') + + local set = Set() + for mbox in pairs(_extract_mailboxes(self)) do + set = set + mbox.match_message(mbox, pattern, self) + end + return self * set +end + + +Set._mt.__call = Set._new diff --git a/signal.c b/signal.c new file mode 100644 index 0000000..229a454 --- /dev/null +++ b/signal.c @@ -0,0 +1,45 @@ +#include + +#include "imapfilter.h" + + +void signal_handler(int sig); + + +/* + * Catch signals that cause program's termination. + */ +void +catch_signals(void) +{ + + signal(SIGINT, signal_handler); + signal(SIGQUIT, signal_handler); + signal(SIGTERM, signal_handler); +} + + +/* + * Release signals and reset them to default action. + */ +void +release_signals(void) +{ + + signal(SIGINT, SIG_DFL); + signal(SIGQUIT, SIG_DFL); + signal(SIGTERM, SIG_DFL); +} + + +/* + * Signal handler for signals that cause termination of program. + */ +void +signal_handler(int sig) +{ + + release_signals(); + + fatal(ERROR_SIGNAL, "killed by signal %d\n", sig); +} diff --git a/socket.c b/socket.c new file mode 100644 index 0000000..04fd6fd --- /dev/null +++ b/socket.c @@ -0,0 +1,440 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "imapfilter.h" +#include "session.h" + +#ifndef NO_SSLTLS +#include +#include +#endif + + +/* + * Connect to mail server. + */ +int +open_connection(session *ssn, const char *server, const char *port, + const char *protocol) +{ + struct addrinfo hints, *res, *ressave; + int n, sockfd; + +#ifdef NO_SSLTLS + if (protocol) { + error("SSL not supported by this build\n"); + return -1; + } +#endif + + memset(&hints, 0, sizeof(struct addrinfo)); + + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + n = getaddrinfo(server, port, &hints, &res); + + if (n < 0) { + error("gettaddrinfo; %s\n", gai_strerror(n)); + return -1; + } + + ressave = res; + + sockfd = -1; + + while (res) { + sockfd = socket(res->ai_family, res->ai_socktype, + res->ai_protocol); + + if (sockfd >= 0) { + if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0) + break; + + sockfd = -1; + } + res = res->ai_next; + } + + if (ressave) + freeaddrinfo(ressave); + + if (sockfd == -1) { + error("error while initiating connection to %s at port %s\n", + server, port); + return -1; + } + + ssn->socket = sockfd; + +#ifndef NO_SSLTLS + if (protocol) { + if (open_secure_connection(ssn, server, port, protocol) == -1) { + close_connection(ssn); + return -1; + } + } +#endif + + return ssn->socket; +} + + +#ifndef NO_SSLTLS +/* + * Initialize SSL/TLS connection. + */ +int +open_secure_connection(session *ssn, const char *server, const char *port, + const char *protocol) +{ + int r, e; + SSL_CTX *ctx; + SSL_METHOD *method; + + method = NULL; + + if (!strncasecmp(protocol, "tls1", 4)) + method = TLSv1_client_method(); + else if (!strncasecmp(protocol, "ssl3", 4) || + !strncasecmp(protocol, "ssl2", 4)) + method = SSLv23_client_method(); + + if (!(ctx = SSL_CTX_new(method))) + goto fail; + + if (!(ssn->ssl = SSL_new(ctx))) + goto fail; + + SSL_set_fd(ssn->ssl, ssn->socket); + + for (;;) { + if ((r = SSL_connect(ssn->ssl)) > 0) + break; + + switch (SSL_get_error(ssn->ssl, r)) { + case SSL_ERROR_ZERO_RETURN: + error("initiating SSL connection to %s; the " + "connection has been closed cleanly\n", server); + goto fail; + case SSL_ERROR_WANT_CONNECT: + case SSL_ERROR_WANT_ACCEPT: + case SSL_ERROR_WANT_X509_LOOKUP: + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + break; + case SSL_ERROR_SYSCALL: + e = ERR_get_error(); + if (e == 0) + error("initiating SSL connection to %s; EOF " + "in violation of the protocol\n", server); + else if (e == -1) + error("initiating SSL connection to %s; %s\n", + server, strerror(errno)); + goto fail; + case SSL_ERROR_SSL: + error("initiating SSL connection to %s; %s\n", server, + ERR_error_string(ERR_get_error(), NULL)); + goto fail; + default: + goto fail; + } + } + if (get_option_boolean("certificates") && get_cert(ssn) == -1) + goto fail; + + SSL_CTX_free(ctx); + + return 0; + +fail: + ssn->ssl = NULL; + SSL_CTX_free(ctx); + + return -1; +} +#endif /* NO_SSLTLS */ + + +/* + * Disconnect from mail server. + */ +int +close_connection(session *ssn) +{ + int r; + + r = 0; + +#ifndef NO_SSLTLS + close_secure_connection(ssn); +#endif + + if (ssn->socket != -1) { + r = close(ssn->socket); + ssn->socket = -1; + + if (r == -1) + error("closing socket; %s\n", strerror(errno)); + } + return r; +} + + +#ifndef NO_SSLTLS +/* + * Shutdown SSL/TLS connection. + */ +int +close_secure_connection(session *ssn) +{ + + if (ssn->ssl) { + SSL_shutdown(ssn->ssl); + SSL_free(ssn->ssl); + ssn->ssl = NULL; + } + + return 0; +} +#endif + + +/* + * Read data from socket. + */ +ssize_t +socket_read(session *ssn, char *buf, size_t len, long timeout, int timeoutfail) +{ + int s; + ssize_t r; + fd_set fds; + + struct timeval tv; + + struct timeval *tvp; + + r = 0; + s = 1; + tvp = NULL; + + memset(buf, 0, len + 1); + + if (timeout > 0) { + tv.tv_sec = timeout; + tv.tv_usec = 0; + tvp = &tv; + } + + FD_ZERO(&fds); + FD_SET(ssn->socket, &fds); + +#ifndef NO_SSLTLS + if (ssn->ssl) { + if (SSL_pending(ssn->ssl) > 0 || + ((s = select(ssn->socket + 1, &fds, NULL, NULL, tvp)) > 0 && + FD_ISSET(ssn->socket, &fds))) { + r = socket_secure_read(ssn, buf, len); + + if (r <= 0) + goto fail; + } + } else +#endif + { + if ((s = select(ssn->socket + 1, &fds, NULL, NULL, tvp)) > 0 && + FD_ISSET(ssn->socket, &fds)) { + r = read(ssn->socket, buf, len); + + if (r == -1) { + error("reading data; %s\n", strerror(errno)); + goto fail; + } else if (r == 0) { + goto fail; + } + } + } + + if (s == -1) { + error("waiting to read from socket; %s\n", strerror(errno)); + goto fail; + } else if (s == 0 && timeoutfail) { + error("timeout period expired while waiting to read data\n"); + goto fail; + } + + return r; +fail: + close_connection(ssn); + + return -1; + +} + + +#ifndef NO_SSLTLS +/* + * Read data from a TLS/SSL connection. + */ +ssize_t +socket_secure_read(session *ssn, char *buf, size_t len) +{ + int r, e; + + for (;;) { + r = (ssize_t) SSL_read(ssn->ssl, buf, len); + + if (r > 0) + break; + + switch (SSL_get_error(ssn->ssl, r)) { + case SSL_ERROR_ZERO_RETURN: + error("reading data; the connection has been closed " + "cleanly\n"); + return -1; + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_CONNECT: + case SSL_ERROR_WANT_ACCEPT: + case SSL_ERROR_WANT_X509_LOOKUP: + break; + case SSL_ERROR_SYSCALL: + e = ERR_get_error(); + if (e == 0) + error("reading data; EOF in violation of the " + "protocol\n"); + else if (e == -1) + error("reading data; %s\n", strerror(errno)); + return -1; + case SSL_ERROR_SSL: + error("reading data; %s\n", + ERR_error_string(ERR_get_error(), NULL)); + return -1; + default: + return -1; + } + } + + return r; +} +#endif + + +/* + * Write data to socket. + */ +ssize_t +socket_write(session *ssn, const char *buf, size_t len) +{ + int s; + ssize_t w, wt; + fd_set fds; + + w = wt = 0; + s = 1; + + FD_ZERO(&fds); + FD_SET(ssn->socket, &fds); + + while (len) { + if ((s = select(ssn->socket + 1, NULL, &fds, NULL, NULL) > 0 && + FD_ISSET(ssn->socket, &fds))) { +#ifndef NO_SSLTLS + if (ssn->ssl) { + w = socket_secure_write(ssn, buf, len); + + if (w <= 0) + goto fail; + } else +#endif + { + w = write(ssn->socket, buf, len); + + if (w == -1) { + error("writing data; %s\n", + strerror(errno)); + goto fail; + } else if (w == 0) { + goto fail; + } + } + + if (w > 0) { + len -= w; + buf += w; + wt += w; + } + } + } + + if (s == -1) { + error("waiting to write to socket; %s\n", strerror(errno)); + goto fail; + } else if (s == 0) { + error("timeout period expired while waiting to write data\n"); + goto fail; + } + + return wt; +fail: + close_connection(ssn); + + return -1; +} + + +#ifndef NO_SSLTLS +/* + * Write data to a TLS/SSL connection. + */ +ssize_t +socket_secure_write(session *ssn, const char *buf, size_t len) +{ + int w, e; + + for (;;) { + w = (ssize_t) SSL_write(ssn->ssl, buf, len); + + if (w > 0) + break; + + switch (SSL_get_error(ssn->ssl, w)) { + case SSL_ERROR_ZERO_RETURN: + error("writing data; the connection has been closed " + "cleanly\n"); + return -1; + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_CONNECT: + case SSL_ERROR_WANT_ACCEPT: + case SSL_ERROR_WANT_X509_LOOKUP: + break; + case SSL_ERROR_SYSCALL: + e = ERR_get_error(); + if (e == 0) + error("writing data; EOF in violation of the " + "protocol\n"); + else if (e == -1) + error("writing data; %s\n", strerror(errno)); + return -1; + case SSL_ERROR_SSL: + error("writing data; %s\n", + ERR_error_string(ERR_get_error(), NULL)); + return -1; + default: + return -1; + } + } + + return w; +} +#endif diff --git a/system.c b/system.c new file mode 100644 index 0000000..73105a5 --- /dev/null +++ b/system.c @@ -0,0 +1,308 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "imapfilter.h" + + +static int ifsys_echo(lua_State *lua); +static int ifsys_noecho(lua_State *lua); +static int ifsys_popen(lua_State *lua); +static int ifsys_pclose(lua_State *lua); +static int ifsys_read(lua_State *lua); +static int ifsys_write(lua_State *lua); +static int ifsys_sleep(lua_State *lua); +static int ifsys_daemon(lua_State *lua); + +/* Lua imapfilter library of system's functions. */ +static const luaL_reg ifsyslib[] = { + { "echo", ifsys_echo }, + { "noecho", ifsys_noecho }, + { "popen", ifsys_popen }, + { "pclose", ifsys_pclose }, + { "read", ifsys_read }, + { "write", ifsys_write }, + { "sleep", ifsys_sleep }, + { "daemon", ifsys_daemon }, + { NULL, NULL } +}; + + +/* + * Enable character echoing. + */ +static int +ifsys_echo(lua_State *lua) +{ + struct termios t; + + if (lua_gettop(lua) != 0) + luaL_error(lua, "wrong number of arguments"); + + if (tcgetattr(fileno(stdin), &t)) { + fprintf(stderr, "getting term attributs; %s\n", + strerror(errno)); + return 1; + } + + t.c_lflag |= (ECHO); + t.c_lflag &= ~(ECHONL); + + if (tcsetattr(fileno(stdin), TCSAFLUSH, &t)) { + fprintf(stderr, "setting term attributes; %s\n", + strerror(errno)); + return 1; + } + + return 0; +} + + +/* + * Disable character echoing. + */ +static int +ifsys_noecho(lua_State *lua) +{ + struct termios t; + + if (lua_gettop(lua) != 0) + luaL_error(lua, "wrong number of arguments"); + + if (tcgetattr(fileno(stdin), &t)) { + fprintf(stderr, "getting term attributs; %s\n", + strerror(errno)); + return 1; + } + + t.c_lflag &= ~(ECHO); + t.c_lflag |= (ECHONL); + + if (tcsetattr(fileno(stdin), TCSAFLUSH, &t)) { + fprintf(stderr, "setting term attributes; %s\n", + strerror(errno)); + return 1; + } + + return 0; +} + + +/* + * Lua implementation of the POSIX popen() function. + */ +static int +ifsys_popen(lua_State *lua) +{ + FILE **fp; + + if (lua_gettop(lua) != 2) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TSTRING); + luaL_checktype(lua, 2, LUA_TSTRING); + + fp = (FILE **) lua_newuserdata(lua, sizeof(FILE *)); + + *fp = NULL; + + *fp = popen(lua_tostring(lua, 1), lua_tostring(lua, 2)); + + lua_remove(lua, 1); + lua_remove(lua, 1); + + return (*fp == NULL ? 0 : 1); +} + + +/* + * Lua implementation of the POSIX pclose() function. + */ +static int +ifsys_pclose(lua_State *lua) +{ + lua_Number r; + FILE *fp; + + if (lua_gettop(lua) != 1) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TUSERDATA); + + fp = *(FILE **) (lua_touserdata(lua, 1)); + + r = (lua_Number) (pclose(fp) >> 8); + + lua_pop(lua, 1); + + if (r == -1) + return 0; + + fp = NULL; + lua_pushnumber(lua, r); + + return 1; +} + + +/* + * Reads a line from a file stream. + */ +static int +ifsys_read(lua_State *lua) +{ + FILE *fp; + luaL_Buffer b; + char *c; + size_t n; + + if (lua_gettop(lua) != 1) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TUSERDATA); + + fp = *(FILE **) (lua_touserdata(lua, 1)); + + lua_pop(lua, 1); + + luaL_buffinit(lua, &b); + + for (;;) { + c = luaL_prepbuffer(&b); + + if (fgets(c, LUAL_BUFFERSIZE, fp) == NULL && feof(fp)) { + luaL_pushresult(&b); + + return (lua_strlen(lua, -1) > 0); + } + n = strlen(c); + + if (c[n - 1] != '\n') + luaL_addsize(&b, n); + else { + luaL_addsize(&b, n); + luaL_pushresult(&b); + + return 1; + } + } +} + + +/* + * Writes a string to a file stream. + */ +static int +ifsys_write(lua_State *lua) +{ + size_t n; + + if (lua_gettop(lua) != 2) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TUSERDATA); + luaL_checktype(lua, 2, LUA_TSTRING); + + n = fwrite(lua_tostring(lua, 2), sizeof(char), strlen(lua_tostring(lua, + 2)), *(FILE **) (lua_touserdata(lua, 1))); + + lua_pop(lua, 2); + + lua_pushboolean(lua, (n != 0)); + + return 1; +} + + +/* + * Lua implementation of the POSIX sleep() function. + */ +static int +ifsys_sleep(lua_State *lua) +{ + + if (lua_gettop(lua) != 1) + luaL_error(lua, "wrong number of arguments"); + + luaL_checktype(lua, 1, LUA_TNUMBER); + + lua_pushnumber(lua, + (lua_Number) (sleep) ((unsigned int)(lua_tonumber(lua, 1)))); + + lua_remove(lua, 1); + + return 1; +} + + +/* + * Lua implementation of the BSD daemon() function. + */ +static int +ifsys_daemon(lua_State *lua) +{ + + if (lua_gettop(lua) != 0) + luaL_error(lua, "wrong number of arguments"); + + switch (fork()) { + case -1: + fprintf(stderr, "forking; %s\n", strerror(errno)); + exit(1); + break; + case 0: + break; + default: + exit(0); + break; + } + + if (setsid() == -1) { + fprintf(stderr, "creating session; %s\n", strerror(errno)); + exit(1); + } + switch (fork()) { + case -1: + fprintf(stderr, "creating session; %s\n", strerror(errno)); + exit(1); + break; + case 0: + break; + default: + exit(0); + break; + } + + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + + if (open("/dev/null", O_RDWR) == -1 || + dup(STDIN_FILENO) == -1 || + dup(STDIN_FILENO) == -1) + fprintf(stderr, "creating session; %s\n", strerror(errno)); + + return 0; +} + + +/* + * Open imapfilter library of system's functions. + */ +LUALIB_API int +luaopen_ifsys(lua_State *lua) +{ + + luaL_register(lua, "ifsys", ifsyslib); + + return 1; +} diff --git a/version.h b/version.h new file mode 100644 index 0000000..cc872d4 --- /dev/null +++ b/version.h @@ -0,0 +1,12 @@ +#ifndef VERSION_H +#define VERSION_H + + +/* Program's version number. */ +#define VERSION "2.2.3" + +/* Program's copyright. */ +#define COPYRIGHT "Copyright (c) 2001-2011 Eleftherios Chatzimparmpas" + + +#endif /* VERSION_H */