--- ./README.md +++ ./README.md @@ -1,3 +1,53 @@ +Apple Push Notification Service (APNS) Plugin +============================================= + +This branch of [dovecot/core](https://github.com/dovecot/core) +includes a plugin for APNS. This is based on +[matthewpowell/pushnotify](https://github.com/matthewpowell/pushnotify), +[cullum/freebsd-ports](https://github.com/cullum/freebsd-ports/tree/master/mail), +and this [post](https://www.c0ffee.net/blog/dovecot-push-notifications/). + +APNS use requires these steps: + +1. Acquire APNS Mail certificates from a (virtual) macOS + High Sierra 10.13 and Server.app version 5.7. Export + the certificates from the Keychain into the file + com.apple.servermgrd.apns.mail.p12 . **Note**: APNS Mail + certificate creation is deprecated on Server.app version 5.8+. +2. Convert the APNS Mail certificates to PEM files: +``` + openssl pkcs12 -in com.apple.servermgrd.apns.mail.p12 \ + -clcerts -nokeys | sed '/BEGIN CERTIFICATE/,$!d' \ + > mail.crt + sudo install -m 0644 -o _dovecot -g _dovenull \ + mail.crt /opt/local/etc/dovecot-apns + openssl pkcs12 -in com.apple.servermgrd.apns.mail.p12 \ + -nodes -nocerts | sed '/BEGIN PRIVATE KEY/,$!d' \ + > mail.key + sudo install -m 0640 -o _dovecot -g _dovenull \ + mail.key /opt/local/etc/dovecot-apns +``` +3. Configure dovecot for APNS: +``` + /opt/local/etc/dovecot/conf.d/15-lda.conf: + protocol lda { mail_plugins = $mail_plugins push_notify } +``` +``` + /opt/local/etc/dovecot/conf.d/90-apns.conf: + aps_topic = com.apple.mail.XServer. +``` + where the certificate's UUID is obtained from the command: +``` + openssl x509 -text -noout \ + -in /opt/local/etc/dovecot-apns/mail.crt \ + | grep -E -o 'com.apple.mail.XServer.[0-9a-f-]+' +``` +4. Launch the APNS daemon (macOS): +``` + sudo launchctl load -w \ + /Library/LaunchDaemons/org.macports.dovecot-apns.plist +``` + Installation ============ --- ./configure.ac +++ ./configure.ac @@ -906,6 +906,7 @@ src/plugins/notify/Makefile src/plugins/notify-status/Makefile src/plugins/push-notification/Makefile +src/plugins/push-notify/Makefile src/plugins/pop3-migration/Makefile src/plugins/quota/Makefile src/plugins/quota-clone/Makefile --- ./src/auth/auth-settings.c +++ ./src/auth/auth-settings.c @@ -271,6 +271,7 @@ DEFLIST(passdbs, "passdb", &auth_passdb_setting_parser_info), DEFLIST(userdbs, "userdb", &auth_userdb_setting_parser_info), + DEF_NOPREFIX(STR, aps_topic), DEF_NOPREFIX(STR, base_dir), DEF_NOPREFIX(BOOL, verbose_proctitle), DEF_NOPREFIX(UINT, first_valid_uid), @@ -333,6 +334,7 @@ .passdbs = ARRAY_INIT, .userdbs = ARRAY_INIT, + .aps_topic = "", .base_dir = PKG_RUNDIR, .verbose_proctitle = FALSE, .first_valid_uid = 500, --- ./src/auth/auth-settings.h +++ ./src/auth/auth-settings.h @@ -82,6 +82,7 @@ ARRAY(struct auth_passdb_settings *) passdbs; ARRAY(struct auth_userdb_settings *) userdbs; + const char *aps_topic; const char *base_dir; const char *ssl_client_ca_dir; const char *ssl_client_ca_file; --- ./src/imap/Makefile.am +++ ./src/imap/Makefile.am @@ -65,6 +65,7 @@ cmd-unselect.c \ cmd-unsubscribe.c \ cmd-urlfetch.c \ + cmd-x-apple-push-service.c \ cmd-x-cancel.c \ cmd-x-state.c --- ./src/imap/cmd-x-apple-push-service.c +++ ./src/imap/cmd-x-apple-push-service.c @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2010-2011 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * 3. Neither the name of Apple Inc. ("Apple") nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include "imap-common.h" +#include "imap-id.h" + +#include "str.h" +#include "home-expand.h" +#include "mail-user.h" +#include "imap-quote.h" + +#include +#include +#include +#include +#include +#include +#include + +#define APS_VERSION "1" + +typedef struct msg_data_s { + unsigned long msg; + unsigned long pid; + + char d1[128]; + char d2[512]; + char d3[512]; + char d4[512]; +} msg_data_t; + +/* + tag1 XAPPLEPUSHSERVICE "aps-version" "1" + "aps-account-id" "E8CD34AD-98D3-4489-A6BB-86B1D082FECE" + "aps-device-token" "a66216ad1683d48b9933cdcc3b98a833ee1a968143f41ea494187da54715da66" + "aps-subtopic" "com.apple.mobilemail" +*/ + +static void do_notify(const char *username, const char *aps_acct_id, + const char *aps_dev_token, const char *aps_sub_topic) +{ + const char *push_notify_path = "@PREFIX@/var/run/dovecot/push_notify"; + + msg_data_t msg_data; + memset(&msg_data, 0, sizeof(struct msg_data_s)); + msg_data.msg = 2; + + strncpy(msg_data.d1, username, sizeof(msg_data.d1)); + strncpy(msg_data.d2, aps_acct_id, sizeof(msg_data.d2)); + strncpy(msg_data.d3, aps_dev_token, sizeof(msg_data.d3)); + strncpy(msg_data.d4, aps_sub_topic, sizeof(msg_data.d4)); + + int soc = socket( AF_UNIX, SOCK_DGRAM, 0 ); + if ( soc < 0 ) { + i_warning( "open notify socket failed(%d): %m", soc ); + return; + } + + struct sockaddr_un sock_addr; + memset( &sock_addr, 0, sizeof(struct sockaddr_un)); + sock_addr.sun_family = AF_UNIX; + strncpy( sock_addr.sun_path, push_notify_path, sizeof(sock_addr.sun_path) ); + socklen_t sock_len = sizeof(sock_addr.sun_family) + strlen(sock_addr.sun_path) + 1; + int rc = connect(soc, (struct sockaddr *) &sock_addr, sock_len); + if ( rc < 0 ) { + i_warning("connect to notify socket %s failed: %m", + push_notify_path); + close(soc); + return; + } + + rc = send(soc, (void *)&msg_data, sizeof(msg_data), 0); + if ( rc < 0 ) + i_warning("send to notify socket %s failed: %m", + push_notify_path); + + close(soc); +} + +static const char *aps_reply_generate (struct client_command_context *cmd, + const struct imap_arg *args) +{ + const char *aps_topic = cmd->client->set->aps_topic; + const char *aps_ver=NULL; + const char *aps_acct_id=NULL; + const char *aps_dev_token=NULL; + const char *aps_sub_topic=NULL; + const char *key, *value; + + /* must have a topic */ + if (aps_topic == NULL || *aps_topic == '\0') + return NULL; + + /* scarf off the aps keys/values */ + while (imap_arg_get_astring(&args[0], &key) && + imap_arg_get_astring(&args[1], &value)) { + if (strcasecmp(key, "aps-version") == 0) + aps_ver = t_strdup(value); + else if (strcasecmp(key, "aps-account-id") == 0) + aps_acct_id = t_strdup(value); + else if (strcasecmp(key, "aps-device-token") == 0) + aps_dev_token = t_strdup(value); + else if (strcasecmp(key, "aps-subtopic") == 0) + aps_sub_topic = t_strdup(value); + else + return NULL; + args += 2; + } + + /* save notification settings */ + if ( aps_ver && aps_acct_id && aps_dev_token && aps_sub_topic ) { + /* subscribe to notification node */ + do_notify(cmd->client->user->username, aps_acct_id, + aps_dev_token, aps_sub_topic); + + /* generate aps response */ + string_t *str = t_str_new(256); + imap_append_quoted( str, "aps-version" ); + str_append_c(str, ' '); + imap_append_quoted( str, APS_VERSION ); + str_append_c(str, ' '); + imap_append_quoted( str, "aps-topic" ); + str_append_c(str, ' '); + imap_append_quoted( str, aps_topic ); + return str_c(str); + } + return NULL; +} + +bool cmd_x_apple_push_service(struct client_command_context *cmd) +{ + const struct imap_arg *args; + + if (!client_read_args(cmd, 0, 0, &args)) + return FALSE; + + const char *reply = aps_reply_generate(cmd, args); + if (reply != NULL) + client_send_line(cmd->client, + t_strdup_printf("* XAPPLEPUSHSERVICE %s", + reply)); + client_send_tagline(cmd, "OK XAPPLEPUSHSERVICE completed."); + + return TRUE; +} --- ./src/imap/imap-client.c +++ ./src/imap/imap-client.c @@ -178,6 +178,8 @@ a chance of working */ client_add_capability(client, "SEARCH=FUZZY"); } + if (set->aps_topic != NULL && *set->aps_topic) + str_append(client->capability_string, " XAPPLEPUSHSERVICE"); mail_set = mail_user_set_get_storage_set(user); if (mail_set->mailbox_list_index) { --- ./src/imap/imap-commands.c +++ ./src/imap/imap-commands.c @@ -76,6 +76,7 @@ { "UID SORT", cmd_sort, COMMAND_FLAG_BREAKS_SEQS }, { "UID THREAD", cmd_thread, COMMAND_FLAG_BREAKS_SEQS }, { "UNSELECT", cmd_unselect, COMMAND_FLAG_BREAKS_MAILBOX }, + { "XAPPLEPUSHSERVICE", cmd_x_apple_push_service, 0}, { "X-CANCEL", cmd_x_cancel, 0 }, { "X-STATE", cmd_x_state, COMMAND_FLAG_REQUIRES_SYNC }, { "XLIST", cmd_list, 0 }, --- ./src/imap/imap-commands.h +++ ./src/imap/imap-commands.h @@ -121,6 +121,7 @@ bool cmd_uid_expunge(struct client_command_context *cmd); bool cmd_move(struct client_command_context *cmd); bool cmd_unselect(struct client_command_context *cmd); +bool cmd_x_apple_push_service(struct client_command_context *cmd); bool cmd_x_cancel(struct client_command_context *cmd); bool cmd_x_state(struct client_command_context *cmd); --- ./src/imap/imap-settings.c +++ ./src/imap/imap-settings.c @@ -75,6 +75,7 @@ DEF(STR, imap_logout_format), DEF(STR, imap_id_send), DEF(STR, imap_id_log), + DEF(STR, aps_topic), DEF(ENUM, imap_fetch_failure), DEF(BOOL, imap_metadata), DEF(BOOL, imap_literal_minus), @@ -103,6 +104,7 @@ "body_count=%{fetch_body_count} body_bytes=%{fetch_body_bytes}", .imap_id_send = "name *", .imap_id_log = "", + .aps_topic = "", .imap_fetch_failure = "disconnect-immediately:disconnect-after:no-after", .imap_metadata = FALSE, .imap_literal_minus = FALSE, --- ./src/imap/imap-settings.h +++ ./src/imap/imap-settings.h @@ -31,6 +31,7 @@ const char *imap_logout_format; const char *imap_id_send; const char *imap_id_log; + const char *aps_topic; const char *imap_fetch_failure; bool imap_metadata; bool imap_literal_minus; --- ./src/imap-login/imap-login-client.c +++ ./src/imap-login/imap-login-client.c @@ -116,6 +116,8 @@ str_append(cap_str, " STARTTLS"); if (is_login_cmd_disabled(client)) str_append(cap_str, " LOGINDISABLED"); + if (*imap_client->set->aps_topic) + str_append(cap_str, " XAPPLEPUSHSERVICE"); client_authenticate_get_capabilities(client, cap_str); return str_c(cap_str); --- ./src/imap-login/imap-login-settings.c +++ ./src/imap-login/imap-login-settings.c @@ -57,6 +57,7 @@ DEF(STR, imap_capability), DEF(STR, imap_id_send), DEF(STR, imap_id_log), + DEF(STR, aps_topic), DEF(BOOL, imap_literal_minus), DEF(BOOL, imap_id_retain), @@ -67,6 +68,7 @@ .imap_capability = "", .imap_id_send = "name *", .imap_id_log = "", + .aps_topic = "", .imap_literal_minus = FALSE, .imap_id_retain = FALSE, }; --- ./src/imap-login/imap-login-settings.h +++ ./src/imap-login/imap-login-settings.h @@ -5,6 +5,7 @@ const char *imap_capability; const char *imap_id_send; const char *imap_id_log; + const char *aps_topic; bool imap_literal_minus; bool imap_id_retain; }; --- ./src/lib-lda/mail-deliver.c +++ ./src/lib-lda/mail-deliver.c @@ -35,6 +35,7 @@ }; deliver_mail_func_t *deliver_mail = NULL; +deliver_hook_func_t *deliver_hook = NULL; struct mail_deliver_mailbox { union mailbox_module_context module_ctx; @@ -461,6 +462,9 @@ } mail_deliver_log(ctx, "saved mail to %s", mailbox_name); pool_unref(&changes.pool); + + if (deliver_hook != NULL) + deliver_hook(ctx, mailbox); } else { mail_deliver_log(ctx, "save failed to %s: %s", mailbox_name, mail_storage_get_last_internal_error(*storage_r, &error)); @@ -549,6 +553,8 @@ /* success. message may or may not have been saved. */ ret = 0; } + if (!ret && deliver_hook) + deliver_hook(ctx, ctx->rcpt_default_mailbox); mail_duplicate_db_deinit(&ctx->dup_db); if (ret < 0 && mail_deliver_is_tempfailed(ctx, *storage_r)) return -1; --- ./src/lib-lda/mail-deliver.h +++ ./src/lib-lda/mail-deliver.h @@ -140,6 +140,9 @@ struct mail_storage **storage_r); extern deliver_mail_func_t *deliver_mail; +typedef void deliver_hook_func_t(struct mail_deliver_context *ctx, + const char *mailbox); +extern deliver_hook_func_t *deliver_hook; const struct var_expand_table * mail_deliver_ctx_get_log_var_expand_table(struct mail_deliver_context *ctx, --- ./src/plugins/Makefile.am +++ ./src/plugins/Makefile.am @@ -29,6 +29,7 @@ notify \ notify-status \ push-notification \ + push-notify \ mail-log \ $(MAIL_LUA) \ mailbox-alias \ --- ./src/plugins/push-notify/Makefile.am +++ ./src/plugins/push-notify/Makefile.am @@ -0,0 +1,25 @@ +AM_CPPFLAGS = \ + -I$(top_srcdir)/src/lib \ + -I$(top_srcdir)/src/lib-lda \ + -I$(top_srcdir)/src/lib-mail \ + -I$(top_srcdir)/src/lib-smtp \ + -I$(top_srcdir)/src/lib-storage + +NOPLUGIN_LDFLAGS = +lib20_push_notify_plugin_la_LDFLAGS = -module -avoid-version + +module_LTLIBRARIES = \ + lib20_push_notify_plugin.la + +if DOVECOT_PLUGIN_DEPS +notify_deps = ../notify/lib15_notify_plugin.la +endif + +lib20_push_notify_plugin_la_SOURCES = \ + push-notify-plugin.c + +headers = \ + push-notify-plugin.h + +pkginc_libdir = $(pkgincludedir) +pkginc_lib_HEADERS = $(headers) --- ./src/plugins/push-notify/push-notify-plugin.c +++ ./src/plugins/push-notify/push-notify-plugin.c @@ -0,0 +1,102 @@ +/* Copyright (c) 2008-2011 Apple, inc. */ + +#include "lib.h" +#include "mail-deliver.h" +#include "mail-namespace.h" +#include "message-address.h" +#include "push-notify-plugin.h" + +#include +#include +#include +#include + +const char *push_notify_plugin_version = DOVECOT_ABI_VERSION; + +static deliver_hook_func_t *next_deliver_mail; +struct et_list *_et_list = NULL; + +struct message_info { + const char *from; + const char *subj; +}; + +// ----------------------------------------------------------------- +// push_notification () + +static void +push_notification(struct mail_deliver_context *ctx, const char *mailbox) +{ + int debug = 0; + int notify_sock = 0; + const char *sock_path = "@PREFIX@/var/run/dovecot/push_notify"; + ssize_t rc = 0; + socklen_t sock_len = 0; + struct sockaddr_un sock_addr; + struct msg_data_s msg_data; + + if (ctx->rcpt_user->mail_debug) { + debug = 1; + i_info( "push-notify: push notification enabled" ); + } + + if ( strcasecmp(mailbox, "INBOX") != 0) { + i_info( "push-notify: message saved to mailbox: %s, no notification sent", mailbox ); + return; + } + + notify_sock = socket( AF_UNIX, SOCK_DGRAM, 0 ); + if ( notify_sock < 0 ) { + /* warn that connect failed but do not fail the plugin or message will not get delivered */ + i_warning( "push-notify: open socket: \"%s\" failed", sock_path ); + return; + } + + sock_addr.sun_family = AF_UNIX; + strncpy( sock_addr.sun_path, "@PREFIX@/var/run/dovecot/push_notify", sizeof(sock_addr.sun_path) ); + sock_len = sizeof(sock_addr.sun_family) + strlen(sock_addr.sun_path) + 1; + rc = connect(notify_sock, (struct sockaddr *) &sock_addr, sock_len); + if ( rc < 0 ) { + /* warn that connect failed but do not fail the plugin or message will not get delivered */ + i_warning( "push-notify: connect() to socket: \"%s\" failed: %m", sock_path ); + return; + } + + memset( &msg_data, 0, sizeof( msg_data ) ); + msg_data.msg = 3; + + /* set user/account id */ + if ( ctx->rcpt_user->username != NULL ) { + strncpy( msg_data.d1, ctx->rcpt_user->username, sizeof(msg_data.d1) ); + if (debug) + i_info( "push-notify: notify: %s", msg_data.d1 ); + } + + rc = send(notify_sock, (void *)&msg_data, sizeof(struct msg_data_s), 0); + if ( rc < 0 ) + i_warning( "push-notify: send() to socket: \"%s\" failed: %m", sock_path ); + + if (debug) + i_info("push-notify: data sent: %lu", rc); + + close(notify_sock); +} // push_notification + +static void push_notify_deliver(struct mail_deliver_context *ctx, + const char *mailbox) +{ + push_notification(ctx, mailbox); + if (next_deliver_mail != NULL) + next_deliver_mail(ctx, mailbox); +} + +void push_notify_plugin_init (struct module *module ATTR_UNUSED) +{ + next_deliver_mail = deliver_hook; + deliver_hook = push_notify_deliver; +} + +void push_notify_plugin_deinit (void) +{ + deliver_hook = next_deliver_mail; +} --- ./src/plugins/push-notify/push-notify-plugin.h +++ ./src/plugins/push-notify/push-notify-plugin.h @@ -0,0 +1,20 @@ +/* Copyright (c) 2008-2011 Apple, inc. */ + +#ifndef __NOTIFY_PLUGIN_H__ +#define __NOTIFY_PLUGIN_H__ + +struct module; +void push_notify_plugin_init(struct module *module); +void push_notify_plugin_deinit(void); + +typedef struct msg_data_s { + unsigned long msg; + unsigned long pid; + + char d1[128]; + char d2[512]; + char d3[512]; + char d4[512]; +} msg_data_t; + +#endif