commit 5d4ed5ed3f055f3dabf2388d8ccd30da81808db5 Author: Lefteris Chatzimparmpas Date: Sun Mar 6 12:58:58 2011 +0100 Initial commit. 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 */