1 /* $NetBSD: ascii_header_text.c,v 1.2 2025/02/25 19:15:45 christos Exp $ */
2
3 /*++
4 /* NAME
5 /* ascii_header_text 3h
6 /* SUMMARY
7 /* message header content formatting
8 /* SYNOPSIS
9 /* #include <ascii_header_text.h>
10 /*
11 /* char *make_ascii_header_text(
12 /* VSTRING *result,
13 /* int flags,
14 /* const char *str)
15 /* DESCRIPTION
16 /* make_ascii_header_text() takes an ASCII input string and formats
17 /* the content for use in a header phrase or comment.
18 /*
19 /* The result value is a pointer to the result buffer string content,
20 /* or null to indicate that no output was produced (the input was
21 /* empty, or all ASCII whitespace).
22 /*
23 /* Arguments:
24 /* .IP result
25 /* The buffer that the output will overwrite. The result is
26 /* null-terminated.
27 /* .IP flags
28 /* One of HDR_FLAG_PHRASE or HDR_FLAG_COMMENT is required. Other
29 /* flags are optional.
30 /* .RS
31 /* .IP HDR_TEXT_FLAG_PHRASE
32 /* Generate header content that will be used as a phrase, for
33 /* example the full name content in "From: full-name <addr-spec>".
34 /* .IP HDR_TEXT_FLAG_COMMENT
35 /* Generate header content that will be used as a comment, for
36 /* example the full name in "From: addr-spec (full-name)".
37 /* .RE
38 /* .IP str
39 /* Pointer to null-terminated input storage.
40 /* DIAGNOSTICS
41 /* Panic: invalid flags argument.
42 /* SEE ALSO
43 /* rfc2047_code(3), encode header content
44 /* LICENSE
45 /* .ad
46 /* .fi
47 /* The Secure Mailer license must be distributed with this software.
48 /* AUTHOR(S)
49 /* Wietse Venema
50 /* IBM T.J. Watson Research
51 /* P.O. Box 704
52 /* Yorktown Heights, NY 10598, USA
53 /*
54 /* Wietse Venema
55 /* Google, Inc.
56 /* 111 8th Avenue
57 /* New York, NY 10011, USA
58 /*
59 /* Wietse Venema
60 /* porcupine.org
61 /*--*/
62
63 /*
64 * System library.
65 */
66 #include <sys_defs.h>
67 #include <ctype.h>
68 #include <string.h>
69
70 /*
71 * Utility library.
72 */
73 #include <ascii_header_text.h>
74 #include <msg.h>
75 #include <stringops.h>
76 #include <vstring.h>
77
78 /*
79 * Global library.
80 */
81 #include <lex_822.h>
82 #include <mail_params.h>
83 #include <tok822.h>
84
85 /*
86 * Self.
87 */
88 #include <ascii_header_text.h>
89
90 /*
91 * SLMs.
92 */
93 #define STR vstring_str
94 #define LEN VSTRING_LEN
95
96 /* make_ascii_header_text - make header text for phrase or comment */
97
make_ascii_header_text(VSTRING * result,int flags,const char * str)98 char *make_ascii_header_text(VSTRING *result, int flags, const char *str)
99 {
100 const char myname[] = "make_ascii_header_text";
101 const char *cp;
102 int ch;
103 int target;
104
105 /*
106 * Quote or escape ASCII-only content. This factors out code from the
107 * Postfix 2.9 cleanup daemon, without introducing visible changes for
108 * text that contains only non-control characters and well-formed
109 * comments. See TODO()s for some basic improvements that would allow
110 * long inputs to be folded over multiple lines.
111 */
112 VSTRING_RESET(result);
113 switch (target = (flags & HDR_TEXT_MASK_TARGET)) {
114
115 /*
116 * Generate text for a phrase (for example, the full name in "From:
117 * full-name <addr-spec>").
118 *
119 * TODO(wietse) add a tok822_externalize() option to replace whitespace
120 * between phrase tokens with newline, so that a long full name can
121 * be folded. This is a user-visible change; do this early in a
122 * development cycle to find out if this breaks compatibility.
123 */
124 case HDR_TEXT_FLAG_PHRASE:{
125 TOK822 *dummy_token;
126 TOK822 *token;
127
128 if (str[strcspn(str, "%!" LEX_822_SPECIALS)] == 0) {
129 token = tok822_scan_limit(str, &dummy_token,
130 var_token_limit);
131 } else {
132 token = tok822_alloc(TOK822_QSTRING, str);
133 }
134 if (token) {
135 tok822_externalize(result, token, TOK822_STR_NONE);
136 tok822_free_tree(token);
137 VSTRING_TERMINATE(result);
138 return (STR(result));
139 } else {
140 /* No output was generated. */
141 return (0);
142 }
143 }
144 break;
145
146 /*
147 * Generate text for comment content, for example, the full name in
148 * "From: addr-spec (full-name)". We do not quote "(", ")", or "\" as
149 * that would be a user-visible change, but we do fix unbalanced
150 * parentheses or a backslash at the end.
151 *
152 * TODO(wietse): Replace whitespace with newline, so that a long full
153 * name can be folded). This is a user-visible change; do this early
154 * in a development cycle to find out if this breaks compatibility.
155 */
156 case HDR_TEXT_FLAG_COMMENT:{
157 int pc;
158
159 for (pc = 0, cp = str; (ch = *cp) != 0; cp++) {
160 if (ch == '\\') {
161 if (cp[1] == 0)
162 continue;
163 VSTRING_ADDCH(result, ch);
164 ch = *++cp;
165 } else if (ch == '(') {
166 pc++;
167 } else if (ch == ')') {
168 if (pc < 1)
169 continue;
170 pc--;
171 }
172 VSTRING_ADDCH(result, ch);
173 }
174 while (pc-- > 0)
175 VSTRING_ADDCH(result, ')');
176 VSTRING_TERMINATE(result);
177 return (LEN(result) && !allspace(STR(result)) ? STR(result) : 0);
178 }
179 break;
180 default:
181 msg_panic("%s: unknown target '0x%x'", myname, target);
182 }
183 }
184
185 #ifdef TEST
186
187 #include <stdlib.h>
188 #include <string.h>
189 #include <msg.h>
190 #include <msg_vstream.h>
191
192 /*
193 * Test structure. Some tests generate their own.
194 */
195 typedef struct TEST_CASE {
196 const char *label;
197 int (*action) (const struct TEST_CASE *);
198 int flags;
199 const char *input;
200 const char *exp_output;
201 } TEST_CASE;
202
203 #define PASS (0)
204 #define FAIL (1)
205
206 #define NO_OUTPUT ((char *) 0)
207
test_make_ascii_header_text(const TEST_CASE * tp)208 static int test_make_ascii_header_text(const TEST_CASE *tp)
209 {
210 static VSTRING *result;
211 const char *got;
212
213 if (result == 0)
214 result = vstring_alloc(100);
215
216 got = make_ascii_header_text(result, tp->flags, tp->input);
217
218 if (!got != !tp->exp_output) {
219 msg_warn("got result ``%s'', want ``%s''",
220 got ? got : "null",
221 tp->exp_output ? tp->exp_output : "null");
222 return (FAIL);
223 }
224 if (got && strcmp(got, tp->exp_output) != 0) {
225 msg_warn("got result ``%s'', want ``%s''", got, tp->exp_output);
226 return (FAIL);
227 }
228 return (PASS);
229 }
230
231 static const TEST_CASE test_cases[] = {
232
233 /*
234 * Phrase tests.
235 */
236 {"phrase_without_special",
237 test_make_ascii_header_text,
238 HDR_TEXT_FLAG_PHRASE, "abc def", "abc def"
239 },
240 {"phrase_with_special",
241 test_make_ascii_header_text,
242 HDR_TEXT_FLAG_PHRASE, "foo@bar", "\"foo@bar\""
243 },
244 {"phrase_with_space_only",
245 test_make_ascii_header_text,
246 HDR_TEXT_FLAG_PHRASE, " ", NO_OUTPUT
247 },
248 {"phrase_empty",
249 test_make_ascii_header_text,
250 HDR_TEXT_FLAG_PHRASE, "", NO_OUTPUT
251 },
252
253 /*
254 * Comment tests.
255 */
256 {"comment_with_unopened_parens",
257 test_make_ascii_header_text,
258 HDR_TEXT_FLAG_COMMENT, ")foo )bar", "foo bar"
259 },
260 {"comment_with_unclosed_parens",
261 test_make_ascii_header_text,
262 HDR_TEXT_FLAG_COMMENT, "(foo (bar", "(foo (bar))"
263 },
264 {"comment_with_backslash_in_text",
265 test_make_ascii_header_text,
266 HDR_TEXT_FLAG_COMMENT, "foo\\bar", "foo\\bar"
267 },
268 {"comment_with_backslash_at_end",
269 test_make_ascii_header_text,
270 HDR_TEXT_FLAG_COMMENT, "foo\\", "foo"
271 },
272 {"comment_with_backslash_backslash_at_end",
273 test_make_ascii_header_text,
274 HDR_TEXT_FLAG_COMMENT, "foo\\\\", "foo\\\\"
275 },
276 {"comment_with_space_only",
277 test_make_ascii_header_text,
278 HDR_TEXT_FLAG_COMMENT, " ", NO_OUTPUT
279 },
280 {"comment_empty",
281 test_make_ascii_header_text,
282 HDR_TEXT_FLAG_COMMENT, "", NO_OUTPUT
283 },
284 {0},
285 };
286
main(int argc,char ** argv)287 int main(int argc, char **argv)
288 {
289 const TEST_CASE *tp;
290 int pass = 0;
291 int fail = 0;
292
293 msg_vstream_init(sane_basename((VSTRING *) 0, argv[0]), VSTREAM_ERR);
294
295 for (tp = test_cases; tp->label != 0; tp++) {
296 int test_failed;
297
298 msg_info("RUN %s", tp->label);
299 test_failed = tp->action(tp);
300 if (test_failed) {
301 msg_info("FAIL %s", tp->label);
302 fail++;
303 } else {
304 msg_info("PASS %s", tp->label);
305 pass++;
306 }
307 }
308 msg_info("PASS=%d FAIL=%d", pass, fail);
309 exit(fail != 0);
310 }
311
312 #endif
313