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 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 255 static const char *lookup(const char *name, int unused_mode, void *context) 256 { 257 return "DUMMY"; 258 } 259 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