group.c 10.9 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
/** @defgroup arpa2group AxeSMTP application for ARPA2 Group Iteration.
 * @{
 *
 * This is an AxeSMTP application that uses ARPA2 Group logic to relay
 * an email to multiple recipients, all group members.  The sender
 * is assumed to be a group member; this specifically means that an
 * Actor is usually setup by a preceding AxeSMTP application arpa2in.
 *
 * Groups have an email address like any plain user.  Single aliases
 * are added to form an Actor Identity for the various Group Members.
 * So, a <group>@<domain> has any number of <group>+<member>@<domain>.
 * As a side-effect of access_comm() in the arpa2in component, there
 * will be an Actor Identity that is specific for the recipient, and
 * if that recipient is a group address then the Actor Identity would
 * provide the Group Member name.  To that end, an attribute provided
 * "=g<group>+<member>" is setup in the Rule in Communication Access.
 *
 * Given that the MAIL FROM address is now an Actor of a Group Member,
 * it is possible to derive both the <group> and <member> value, and
 * to test if the same <group> and @<domain> are used in the RCPT TO
 * address; if so, then communication to the group is found (this is
 * the only sensible situation for a group, but it helps to recognise
 * potential group traffic; a later database lookup with confirm this
 * more definitively).
 *
 * Knowing that the <member> holds no '+' symbol and through that
 * learning the only possible <group> name, it is now possible to split
 * the RCPT TO as <group><filter>@<domain> where <filter> may be empty
 * to address all listening <group>@<domain> members, or it may start
 * with a '+' symbol and continue to name members, and/or '-' switches
 * between additive and subtractive modes.  This material is used in
 * the group_iterate() call as filters.  It is generally useful to
 * collect all filters for all recipients, so as to avoid sending the
 * same information more than once.  When all RCPT TO addresses from
 * to same MAIL FROM and destined for the same group are combined,
 * and their <filter> strings supplied in one call to group_iterate(),
 * then the least risk remains of duplicate emails at any recipient.
 * This is due to the iteration over the group members and matching
 * them against <filter> strings, rather than unfolding the latter
 * and than pushing out emails independently.
 *
 * The group_iterate() call triggers a callback for any message to
 * be sent, and in doing so it provides the Actor Identities, so
 * the Member Group Identities, of the sender and recipient.  It
 * also provides a delivery address of the form <user>@<domain>
 * that can be used for email delivery, and so as the envelope in
 * outgoing RCPT TO invocations.
 *
 * Group members are setup with %MARKS that flag how they should be
 * used in a number of uses.  For an email list, the following marks
 * matter:
 *
 *   - GROUP_RECV marks members to receive mail from other members
 *   - GROUP_VISITOR marks members to receive mail from non-members
 *
 * Note that visitors should use a dynamic address in the MAIL FROM
 * address, so they too ue an Actor Identity that is a Group Member.
 * Considering the RCPT TO address as a group address with an empty
 * <filter> part allows the lookup and iteration of a group.  It is
 * up to Communication Access who may visit a group.
 *
 * Groups are stored in the ACLdb, but as another Rule Type and,
 * as a result, with different Service Keys.  As a result it is
 * possible to keep it separately owned from Communication Access.
 * This serves the privacy of the iteration, and makes it only
 * available to a tool like this.  The Access Name used in the
 * lookup is the <group> name.
 *
 * This command runs as a post-queue SMTP/LMTP server with any number of
 * SMTP/LMTP clients to pass the traffic out for varying RCPT TO names.
 * Note that LMTP is able to report success or failure for each of the
 * addressed group members separately.  It will not return their delivery
 * address but their Actor Identity, so the Group Member name.
 *
 * SPDX-License-Identifier: BSD-2-Clause
 * SPDX-FileCopyrightText: Copyright 2021 Rick van Rein <rick@openfortress.nl>
 */


#include <stdint.h>
#include <stdbool.h>

#include <errno.h>

#include <ev.h>

#include <arpa2/quick-mem.h>
#include <arpa2/identity.h>
#include <arpa2/rules.h>
#include <arpa2/rules_db.h>
// #include <arpa2/access_comm.h>
#include <arpa2/group.h>

#include <com_err/arpa2identity.h>
#include <com_err/arpa2access.h>

#include "../axe.h"


struct memberfilter {
	struct memberfilter *next;
	a2act_t member;
};

struct appdata_front {
	bool processgroup;
	struct memberfilter *members;
};



/** @brief Process MAIL FROM but stop group processing if it is not a Group Member.
 */
bool arpa2group_mail_from (struct smtp_front *front, const char *addrbuf, unsigned addrlen) {
	if (addrbuf == NULL) {
		// Accept empty address
		memset (&front->from, 0, sizeof (front->from));
	} else if (a2act_parse (&front->from, addrbuf, addrlen, 1)) {
		front->appdata->processgroup = true;
	} else {
		strcpy (front->cmdbuf, "553 Sender address must be an ARPA2 Group Member\r\n");
		return false;
	}
	return true;
}


/** @brief Process RCPT TO as messages to group members.
 *
 * As a result of recipient-specific adaptations to the sender address, there are
 * no situations where RCPT TO would not start with the group, followed by either
 * a '+' or '@' symbol.
 *
 * The recipient addresses are saved until the DATA command, where the set of
 * filters can be composed to support a single group iteration and thereby
 * avoid double message deliveries due to overlap in, say, To: and Cc: headers.
 */
bool arpa2group_rcpt_to (struct smtp_front *front, const char *addrbuf, unsigned addrlen) {
	//
	// Parse the recipient address as minimal ARPA2 Actor which we will later
	// correct by copying A2ID_OFS_PLUS_ALIASES and A2ID_OFS_ALIASES
	a2act_t to;
	if (!a2act_parse (&to, addrbuf, addrlen, 0)) {
		//
		// We got an address but it is not an ARPA2 Identity
		strcpy (front->cmdbuf, "553 Recipient address must be an ARPA2 Group Member\r\n");
		return false;
	}
	//
	// Check that the group and domain are the same as in the sender
	bool same = true;
	same = same && (0 == strcmp (
			front->from.txt + front->from.ofs [A2ID_OFS_AT_DOMAIN],
			to         .txt + to         .ofs [A2ID_OFS_AT_DOMAIN]));
	unsigned aliofs = front->from.ofs [A2ID_OFS_PLUS_ALIASES];
	same = same && (0 == strncmp (front->from.txt, to.txt, aliofs));
	same = same && ((to.txt [aliofs] == '+') || (to.txt [aliofs] == '@'));
	if (!same) {
		//
		// Reject traffic aimed at a different <group>...@<domain>
		strcpy (front->cmdbuf, "553 Sender and recipient address must be in the same ARPA2 Group\r\n");
		return false;
	}
	//
	// Allocate memory and store the recipient for later
	struct memberfilter *newmem = NULL;
	if (!mem_alloc (front, sizeof (struct memberfilter), (void **) &newmem)) {
		strcpy (front->cmdbuf, "452 Insufficient storage for recipient\r\n");
		return false;
	}
	memcpy (&newmem->member, &to, sizeof (a2act_t));
	newmem->next = front->appdata->members;
	front->appdata->members = newmem;
	//
	// Report success
	return true;
}


/** @brief Callback made for every recipient to deliver to.
 *
 * Group Iteration yields members as well as their delivery address.
 * Start a backend for each member, either a fresh or shared connection.
 */
bool arpa2group_member_cb (void *updata, group_marks marks,
				const a2act_t *sender, const a2act_t *recipient,
				const char *delivery, unsigned deliverylen) {
	//
	// Initialise
	struct smtp_front *front = updata;
	//
	// Report for logging
	log_debug ("ARPA2 Group Iteration sender=%s, recipient=%s",
			sender->txt, recipient->txt);
	//
196
197
198
199
200
201
202
203
204
	// Parse the delivery address
	a2id_t rcptto;
	if (!a2id_parse_remote (&rcptto, delivery, deliverylen)) {
		//
		// Invalid delivery address
		strcpy (front->cmdbuf, "553 Member delivery address malformed\r\n");
		return false;
	}
	//
205
206
	// Create a new backend
	struct smtp_back *back = NULL;
207
	if (!back_new (&back, front, sender, &rcptto)) {
208
209
		//
		// Unable to allocate memory
210
		strcpy (front->cmdbuf, "452 Memory exhausted while adding member\r\n");
211
212
213
		return false;
	}
	//
214
215
216
	// Stop processing events until the backend is called
	front_lock (front);
	//
217
218
219
220
	// Connect the backend -- TODO: TRY TO SHARE
	if (!back_connect_new (back, front->client_sockaddr)) {
		//
		// Unable to setup connection
221
222
		static const char *backfail = "450 Connection to backend server failed\r\n";
		front_unlock (front, backfail, strlen (backfail));
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
		return false;
	}
	//
	// Return success
	return true;
}


/** @brief Process DATA by first setting up the desired backends.
 *
 * This combines all filter strings and makes one call to group_iterate().
 * The callbacks from this iteration then launch backends for the various
 * delivery addresses.  When all this succeeds, DATA returns the customary
 * "354 Mail Input", but it may also return "554 Transaction Failed" now.
 */
bool arpa2group_data (struct smtp_front *front) {
	//
	// Count the number of filters
	unsigned numflt = 0;
	for (struct memberfilter *mf = front->appdata->members;
			mf != NULL; mf = mf->next) {
		numflt++;
	}
	//
	// Create the filter list
	const char *filters [numflt + 1];
	unsigned fltctr = numflt;
	filters [numflt--] = NULL;
	for (struct memberfilter *mf = front->appdata->members;
			mf != NULL; mf = mf->next) {
		filters [numflt--] = mf->member.txt;
	}
	//
	// Set the required and forbidden marks
	group_marks required  = 0;
	group_marks forbidden = 0;
	//
	// Lock the frontend, to be unlocked below
261
	front_lock (front);
262
263
264
265
266
267
268
	//
	// Iterate over the group to construct backends
	if (!group_iterate (&front->from, filters,
			required, forbidden,
			NULL, 0,	/* default service key for domain */
			NULL, 0,	/* no contextually defined rules */
			arpa2group_member_cb, front)) {
269
270
		strcpy (front->cmdbuf, "554 Failed during Group Iteration\r\n");
		front_unlock (front, NULL, 0);
271
272
273
274
275
		return false;
	}
	//
	// Test if Group Members were found
	if (front->backends == NULL) {
276
		strcpy (front->cmdbuf, "554 Recipient addresses did not reach any ARPA2 Group Member\r\n");
277
		front_unlock (front, NULL, 0);
278
279
280
		return false;
	}
	//
281
	// Unlock the frontend
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
	static const char *okay = "250 Ok\r\n";
	front_unlock (front, okay, strlen (okay));
	//
	// Return success
	return true;
}


static const struct app_conf arpa2group_conf = {
	.name = "arpa2group",
	.descr = "When the recipient address is a group and the sender is a member of that group, then iterate over the group members and apply filters to distribute the email over group members; TODO: simply pass through non-group emails",
	.cb_mail_from = arpa2group_mail_from,
	.cb_rcpt_to = arpa2group_rcpt_to,
	.cb_data = arpa2group_data,
	.front_appsize = sizeof (struct appdata_front),
};


/** @brief Application call to initialise a server structure.
 *
 * This call sets up the application-specific configuration pointer.
 */
void axesmtp_init (struct smtp_front *front) {
	access_init ();
	group_init ();
	front->conf = &arpa2group_conf;
}


/** @} */