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