1 /* $NetBSD: smtp_reply_footer.c,v 1.3 2020/03/18 19:05:16 christos Exp $ */
2
3 /*++
4 /* NAME
5 /* smtp_reply_footer 3
6 /* SUMMARY
7 /* SMTP reply footer text support
8 /* SYNOPSIS
9 /* #include <smtp_reply_footer.h>
10 /*
11 /* int smtp_reply_footer(buffer, start, template, filter,
12 /* lookup, context)
13 /* VSTRING *buffer;
14 /* ssize_t start;
15 /* const char *template;
16 /* const char *filter;
17 /* const char *(*lookup) (const char *name, void *context);
18 /* void *context;
19 /* DESCRIPTION
20 /* smtp_reply_footer() expands a reply template, and appends
21 /* the result to an existing reply text.
22 /*
23 /* Arguments:
24 /* .IP buffer
25 /* Result buffer. This should contain a properly formatted
26 /* one-line or multi-line SMTP reply, with or without the final
27 /* <CR><LF>. The reply code and optional enhanced status code
28 /* will be replicated in the footer text. One space character
29 /* after the SMTP reply code is replaced by '-'. If the existing
30 /* reply ends in <CR><LF>, the result text will also end in
31 /* <CR><LF>.
32 /* .IP start
33 /* The beginning of the SMTP reply that the footer will be
34 /* appended to. This supports applications that buffer up
35 /* multiple responses in one buffer.
36 /* .IP template
37 /* Template text, with optional $name attributes that will be
38 /* expanded. The two-character sequence "\n" is replaced by a
39 /* line break followed by a copy of the original SMTP reply
40 /* code and optional enhanced status code.
41 /* The two-character sequence "\c" at the start of the template
42 /* suppresses the line break between the reply text and the
43 /* template text.
44 /* .IP filter
45 /* The set of characters that are allowed in attribute expansion.
46 /* .IP lookup
47 /* Attribute name/value lookup function. The result value must
48 /* be a null for a name that is not found, otherwise a pointer
49 /* to null-terminated string.
50 /* .IP context
51 /* Call-back context for the lookup function.
52 /* SEE ALSO
53 /* mac_expand(3) macro expansion
54 /* DIAGNOSTICS
55 /* smtp_reply_footer() returns 0 upon success, -1 if the existing
56 /* reply text is malformed, -2 in the case of a template macro
57 /* parsing error (an undefined macro value is not an error).
58 /*
59 /* Fatal errors: memory allocation problem.
60 /* LICENSE
61 /* .ad
62 /* .fi
63 /* The Secure Mailer license must be distributed with this software.
64 /* AUTHOR(S)
65 /* Wietse Venema
66 /* IBM T.J. Watson Research
67 /* P.O. Box 704
68 /* Yorktown Heights, NY 10598, USA
69 /*
70 /* Wietse Venema
71 /* Google, Inc.
72 /* 111 8th Avenue
73 /* New York, NY 10011, USA
74 /*--*/
75
76 /* System library. */
77
78 #include <sys_defs.h>
79 #include <string.h>
80 #include <ctype.h>
81
82 /* Utility library. */
83
84 #include <msg.h>
85 #include <mymalloc.h>
86 #include <vstring.h>
87
88 /* Global library. */
89
90 #include <dsn_util.h>
91 #include <smtp_reply_footer.h>
92
93 /* SLMs. */
94
95 #define STR vstring_str
96
smtp_reply_footer(VSTRING * buffer,ssize_t start,const char * template,const char * filter,MAC_EXP_LOOKUP_FN lookup,void * context)97 int smtp_reply_footer(VSTRING *buffer, ssize_t start,
98 const char *template,
99 const char *filter,
100 MAC_EXP_LOOKUP_FN lookup,
101 void *context)
102 {
103 const char *myname = "smtp_reply_footer";
104 char *cp;
105 char *next;
106 char *end;
107 ssize_t dsn_len; /* last status code length */
108 ssize_t dsn_offs = -1; /* last status code offset */
109 int crlf_at_end = 0;
110 ssize_t reply_code_offs = -1; /* last SMTP reply code offset */
111 ssize_t reply_patch_undo_len; /* length without final CRLF */
112 int mac_expand_error = 0;
113 int line_added;
114 char *saved_template;
115
116 /*
117 * Sanity check.
118 */
119 if (start < 0 || start > VSTRING_LEN(buffer))
120 msg_panic("%s: bad start: %ld", myname, (long) start);
121 if (*template == 0)
122 msg_panic("%s: empty template", myname);
123
124 /*
125 * Scan the original response without making changes. If the response is
126 * not what we expect, report an error. Otherwise, remember the offset of
127 * the last SMTP reply code.
128 */
129 for (cp = STR(buffer) + start, end = cp + strlen(cp);;) {
130 if (!ISDIGIT(cp[0]) || !ISDIGIT(cp[1]) || !ISDIGIT(cp[2])
131 || (cp[3] != ' ' && cp[3] != '-'))
132 return (-1);
133 reply_code_offs = cp - STR(buffer);
134 if ((next = strstr(cp, "\r\n")) == 0) {
135 next = end;
136 break;
137 }
138 cp = next + 2;
139 if (cp == end) {
140 crlf_at_end = 1;
141 break;
142 }
143 }
144 if (reply_code_offs < 0)
145 return (-1);
146
147 /*
148 * Truncate text after the first null, and truncate the trailing CRLF.
149 */
150 if (next < vstring_end(buffer))
151 vstring_truncate(buffer, next - STR(buffer));
152 reply_patch_undo_len = VSTRING_LEN(buffer);
153
154 /*
155 * Append the footer text one line at a time. Caution: before we append
156 * parts from the buffer to itself, we must extend the buffer first,
157 * otherwise we would have a dangling pointer "read" bug.
158 *
159 * XXX mac_expand() has no template length argument, so we must
160 * null-terminate the template in the middle.
161 */
162 dsn_offs = reply_code_offs + 4;
163 dsn_len = dsn_valid(STR(buffer) + dsn_offs);
164 line_added = 0;
165 saved_template = mystrdup(template);
166 for (cp = saved_template, end = cp + strlen(cp);;) {
167 if ((next = strstr(cp, "\\n")) != 0) {
168 *next = 0;
169 } else {
170 next = end;
171 }
172 if (cp == saved_template && strncmp(cp, "\\c", 2) == 0) {
173 /* Handle \c at start of template. */
174 cp += 2;
175 } else {
176 /* Append a clone of the SMTP reply code. */
177 vstring_strcat(buffer, "\r\n");
178 VSTRING_SPACE(buffer, 3);
179 vstring_strncat(buffer, STR(buffer) + reply_code_offs, 3);
180 vstring_strcat(buffer, next != end ? "-" : " ");
181 /* Append a clone of the optional enhanced status code. */
182 if (dsn_len > 0) {
183 VSTRING_SPACE(buffer, dsn_len);
184 vstring_strncat(buffer, STR(buffer) + dsn_offs, dsn_len);
185 vstring_strcat(buffer, " ");
186 }
187 line_added = 1;
188 }
189 /* Append one line of footer text. */
190 mac_expand_error = (mac_expand(buffer, cp, MAC_EXP_FLAG_APPEND, filter,
191 lookup, context) & MAC_PARSE_ERROR);
192 if (mac_expand_error)
193 break;
194 if (next < end) {
195 cp = next + 2;
196 } else
197 break;
198 }
199 myfree(saved_template);
200 /* Discard appended text after error, or finalize the result. */
201 if (mac_expand_error) {
202 vstring_truncate(buffer, reply_patch_undo_len);
203 VSTRING_TERMINATE(buffer);
204 } else if (line_added > 0) {
205 STR(buffer)[reply_code_offs + 3] = '-';
206 }
207 /* Restore CRLF at end. */
208 if (crlf_at_end)
209 vstring_strcat(buffer, "\r\n");
210 return (mac_expand_error ? -2 : 0);
211 }
212
213 #ifdef TEST
214
215 #include <stdlib.h>
216 #include <unistd.h>
217 #include <string.h>
218 #include <msg.h>
219 #include <vstream.h>
220 #include <vstring_vstream.h>
221 #include <msg_vstream.h>
222
223 struct test_case {
224 const char *title;
225 const char *orig_reply;
226 const char *template;
227 const char *filter;
228 int expected_status;
229 const char *expected_reply;
230 };
231
232 #define NO_FILTER ((char *) 0)
233 #define NO_TEMPLATE "NO_TEMPLATE"
234 #define NO_ERROR (0)
235 #define BAD_SMTP (-1)
236 #define BAD_MACRO (-2)
237
238 static const struct test_case test_cases[] = {
239 {"missing reply", "", NO_TEMPLATE, NO_FILTER, BAD_SMTP, 0},
240 {"long smtp_code", "1234 foo", NO_TEMPLATE, NO_FILTER, BAD_SMTP, 0},
241 {"short smtp_code", "12 foo", NO_TEMPLATE, NO_FILTER, BAD_SMTP, 0},
242 {"good+bad smtp_code", "321 foo\r\n1234 foo", NO_TEMPLATE, NO_FILTER, BAD_SMTP, 0},
243 {"1-line no dsn", "550 Foo", "\\c footer", NO_FILTER, NO_ERROR, "550 Foo footer"},
244 {"1-line no dsn", "550 Foo", "Bar", NO_FILTER, NO_ERROR, "550-Foo\r\n550 Bar"},
245 {"2-line no dsn", "550-Foo\r\n550 Bar", "Baz", NO_FILTER, NO_ERROR, "550-Foo\r\n550-Bar\r\n550 Baz"},
246 {"1-line with dsn", "550 5.1.1 Foo", "Bar", NO_FILTER, NO_ERROR, "550-5.1.1 Foo\r\n550 5.1.1 Bar"},
247 {"2-line with dsn", "550-5.1.1 Foo\r\n450 4.1.1 Bar", "Baz", NO_FILTER, NO_ERROR, "550-5.1.1 Foo\r\n450-4.1.1 Bar\r\n450 4.1.1 Baz"},
248 {"bad macro", "220 myhostname", "\\c ${whatever", NO_FILTER, BAD_MACRO, 0},
249 {"bad macroCRLF", "220 myhostname\r\n", "\\c ${whatever", NO_FILTER, BAD_MACRO, 0},
250 {"good macro", "220 myhostname", "\\c $whatever", NO_FILTER, NO_ERROR, "220 myhostname DUMMY"},
251 {"good macroCRLF", "220 myhostname\r\n", "\\c $whatever", NO_FILTER, NO_ERROR, "220 myhostname DUMMY\r\n"},
252 0,
253 };
254
lookup(const char * name,int unused_mode,void * context)255 static const char *lookup(const char *name, int unused_mode, void *context)
256 {
257 return "DUMMY";
258 }
259
main(int argc,char ** argv)260 int main(int argc, char **argv)
261 {
262 const struct test_case *tp;
263 int status;
264 VSTRING *buf = vstring_alloc(10);
265 void *context = 0;
266
267 msg_vstream_init(argv[0], VSTREAM_ERR);
268
269 for (tp = test_cases; tp->title != 0; tp++) {
270 vstring_strcpy(buf, tp->orig_reply);
271 status = smtp_reply_footer(buf, 0, tp->template, tp->filter,
272 lookup, context);
273 if (status != tp->expected_status) {
274 msg_warn("test \"%s\": status %d, expected %d",
275 tp->title, status, tp->expected_status);
276 } else if (status < 0 && strcmp(STR(buf), tp->orig_reply) != 0) {
277 msg_warn("test \"%s\": result \"%s\", expected \"%s\"",
278 tp->title, STR(buf), tp->orig_reply);
279 } else if (status == 0 && strcmp(STR(buf), tp->expected_reply) != 0) {
280 msg_warn("test \"%s\": result \"%s\", expected \"%s\"",
281 tp->title, STR(buf), tp->expected_reply);
282 } else {
283 msg_info("test \"%s\": pass", tp->title);
284 }
285 }
286 vstring_free(buf);
287 exit(0);
288 }
289
290 #endif
291