THANKS: Coverity.com (overdue)
[s-mailx.git] / src / mx / mta-aliases.c
blobc969c658e92664ba08583e2b0b1cfd6bd780b559
1 /*@ S-nail - a mail user agent derived from Berkeley Mail.
2 *@ Implementation of mta-aliases.h. XXX Support multiple files
3 *@ TODO With an on_loop_tick_event, trigger cache update once per loop max.
5 * Copyright (c) 2019 - 2020 Steffen (Daode) Nurpmeso <steffen@sdaoden.eu>.
6 * SPDX-License-Identifier: ISC
8 * Permission to use, copy, modify, and/or distribute this software for any
9 * purpose with or without fee is hereby granted, provided that the above
10 * copyright notice and this permission notice appear in all copies.
12 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
13 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
14 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
15 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
16 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
17 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
18 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
20 #undef su_FILE
21 #define su_FILE mta_aliases
22 #define mx_SOURCE
23 #define mx_SOURCE_MTA_ALIASES
25 #ifndef mx_HAVE_AMALGAMATION
26 # include "mx/nail.h"
27 #endif
29 su_EMPTY_FILE()
30 #ifdef mx_HAVE_MTA_ALIASES
31 #include <sys/stat.h> /* TODO su_path_info */
33 #include <su/cs.h>
34 #include <su/cs-dict.h>
35 #include <su/mem.h>
37 #include "mx/cmd.h"
38 #include "mx/file-streams.h"
39 #include "mx/names.h"
40 #include "mx/termios.h"
42 #include "mx/mta-aliases.h"
43 #include "su/code-in.h"
45 enum a_mtaali_type{
46 a_MTAALI_T_NAME,
47 a_MTAALI_T_ADDR,
48 a_MTAALI_T_FILE,
49 a_MTAALI_T_PIPE
52 struct a_mtaali_g{
53 char *mag_path; /* MTA alias file path, expanded (and init switch) */
54 struct a_mtaali_alias *mag_aliases; /* In parse order */
55 /* We store n_strlist values which are set to "name + NUL + ENUM",
56 * where ENUM is enum a_mtaali_type (NAME needs recursion).
57 * The first entry also has memory to store the mtaali_alias */
58 struct su_cs_dict mag_dict;
60 #define a_MTAALI_G_ERR R(char*,-1) /* .mag_path */
62 struct a_mtaali_alias{
63 struct a_mtaali_alias *maa_next;
64 char const *maa_key;
65 /* Values are set to "name + NUL + ENUM", where ENUM is enum a_mtaali_type
66 * (NAME needs recursion).
67 * The first entry also provides memory to store the mtaali_alias */
68 struct n_strlist *maa_values;
71 struct a_mtaali_stack{
72 char const *mas_path_usr; /* Unexpanded version */
73 char const *mas_path;
74 struct a_mtaali_alias *mas_aliases;
75 struct su_cs_dict mas_dict;
78 struct a_mtaali_query{
79 struct su_cs_dict *maq_dp;
80 struct mx_name *maq_result;
81 s32 maq_err;
82 u32 maq_type;
85 static struct a_mtaali_g a_mtaali_g; /* XXX debug atexit */
87 static void a_mtaali_gut_csd(struct su_cs_dict *csdp);
89 static s32 a_mtaali_cache_init(char const *usrfile);
90 static s32 a_mtaali__read_file(struct a_mtaali_stack *masp);
92 static void a_mtaali_expand(uz lvl, char const *name,
93 struct a_mtaali_query *maqp);
95 static void
96 a_mtaali_gut_csd(struct su_cs_dict *csdp){
97 struct su_cs_dict_view csdv;
98 NYD2_IN;
100 su_CS_DICT_FOREACH(csdp, &csdv){
101 union {void *v; struct a_mtaali_alias *maa; struct n_strlist *sl;} p;
103 p.v = su_cs_dict_view_data(&csdv);
105 for(p.sl = p.maa->maa_values; p.sl != NIL;){
106 struct n_strlist *tmp;
108 tmp = p.sl;
109 p.sl = p.sl->sl_next;
110 su_FREE(tmp);
114 su_cs_dict_gut(csdp);
115 NYD2_OU;
118 static s32
119 a_mtaali_cache_init(char const *usrfile){
120 struct a_mtaali_stack mas;
121 s32 rv;
122 NYD_IN;
124 if((mas.mas_path =
125 fexpand(mas.mas_path_usr = usrfile, (FEXP_NOPROTO | FEXP_LOCAL_FILE |
126 FEXP_NSHELL))) == NIL){
127 rv = su_ERR_NOENT;
128 goto jerr;
129 }else if(a_mtaali_g.mag_path == NIL ||
130 a_mtaali_g.mag_path == a_MTAALI_G_ERR ||
131 su_cs_cmp(mas.mas_path, a_mtaali_g.mag_path)){
132 if((rv = a_mtaali__read_file(&mas)) != su_ERR_NONE)
133 goto jerr_nolog;
135 if(a_mtaali_g.mag_path != NIL && a_mtaali_g.mag_path != a_MTAALI_G_ERR){
136 su_FREE(a_mtaali_g.mag_path);
137 a_mtaali_gut_csd(&a_mtaali_g.mag_dict);
139 a_mtaali_g.mag_path = su_cs_dup(mas.mas_path, 0);
140 a_mtaali_g.mag_aliases = mas.mas_aliases;
141 a_mtaali_g.mag_dict = mas.mas_dict;
142 }else
143 rv = su_ERR_NONE;
145 jleave:
146 NYD_OU;
147 return rv;
149 jerr:
150 n_err(_("*mta_aliases*: %s: %s\n"),
151 n_shexp_quote_cp(mas.mas_path_usr, FAL0), su_err_doc(rv));
152 jerr_nolog:
153 if(a_mtaali_g.mag_path != NIL && a_mtaali_g.mag_path != a_MTAALI_G_ERR){
154 a_mtaali_gut_csd(&mas.mas_dict);
155 su_FREE(a_mtaali_g.mag_path);
157 a_mtaali_g.mag_path = a_MTAALI_G_ERR;
158 goto jleave;
161 static s32
162 a_mtaali__read_file(struct a_mtaali_stack *masp){
163 struct str line, l;
164 struct su_cs_dict_view csdv;
165 struct a_mtaali_alias **maapp, *maap;
166 struct n_string ns, *nsp;
167 struct su_cs_dict *dp;
168 s32 rv;
169 FILE *afp;
170 NYD_IN;
172 if((afp = mx_fs_open(masp->mas_path, "r")) == NIL){
173 rv = su_err_no();
174 n_err(_("*mta-aliases*: cannot open %s: %s\n"),
175 n_shexp_quote_cp(masp->mas_path_usr, FAL0), su_err_doc(rv));
176 goto jleave;
179 dp = su_cs_dict_create(&masp->mas_dict,
180 (su_CS_DICT_POW2_SPACED | su_CS_DICT_CASE), NIL);
181 su_cs_dict_view_setup(&csdv, dp);
182 nsp = n_string_creat_auto(&ns);
183 nsp = n_string_book(nsp, 512);
185 /* Read in the database */
186 mx_fs_linepool_aquire(&line.s, &line.l);
188 masp->mas_aliases = NIL;
189 maapp = &masp->mas_aliases;
191 while((rv = readline_restart(afp, &line.s, &line.l, 0)) >= 0){
192 /* :: According to Postfix aliases(5) */
193 l.s = line.s;
194 l.l = S(uz,rv);
195 n_str_trim(&l, n_STR_TRIM_BOTH);
197 /* :
198 * Empty lines and whitespace-only lines are ignored, as are lines
199 * whose first non-whitespace character is a `#'. */
200 if(l.l == 0 || l.s[0] == '#')
201 continue;
203 /* :
204 * A logical line starts with non-whitespace text. A line that starts
205 * with whitespace continues a logical line. */
206 if(l.s != line.s || nsp->s_len == 0){
207 if(!n_string_can_book(nsp, l.l)){
208 su_state_err(su_STATE_ERR_OVERFLOW, (su_STATE_ERR_PASS |
209 su_STATE_ERR_NOERRNO), _("*mta-aliases*: line too long"));
210 l.s = UNCONST(char*,N_("line too long"));
211 rv = su_ERR_OVERFLOW;
212 goto jparse_err;
214 nsp = n_string_push_buf(nsp, l.s, l.l);
215 continue;
218 ASSERT(nsp->s_len > 0);
219 jparse_line:{
220 /* :
221 * An alias definition has the form
222 * name: value1, value2, ...
223 * ...
224 * The name is a local address (no domain part). Use double quotes
225 * when the name contains any special characters such as
226 * whitespace, `#', `:', or `@'. The name is folded to lowercase,
227 * in order to make database lookups case insensitive.
228 * XXX Don't support quoted names nor special characters (manual!) */
229 struct str l2;
230 char c;
232 l2.l = nsp->s_len;
233 l2.s = n_string_cp(nsp);
235 if((l2.s = su_cs_find_c(l2.s, ':')) == NIL ||
236 (l2.l = P2UZ(l2.s - nsp->s_dat)) == 0){
237 l.s = UNCONST(char*,N_("invalid line"));
238 rv = su_ERR_INVAL;
239 goto jparse_err;
242 /* XXX Manual! "name" may only be a Unix username (useradd(8)):
243 * Usernames must start with a lower case letter or an underscore,
244 * followed by lower case letters, digits, underscores, or dashes.
245 * They can end with a dollar sign. In regular expression terms:
246 * [a-z_][a-z0-9_-]*[$]?
247 * Usernames may only be up to 32 characters long.
248 * Test against alpha since the csdict will lowercase names.. */
249 *l2.s = '\0';
250 c = *(l2.s = nsp->s_dat);
251 if(!su_cs_is_alpha(c) && c != '_'){
252 jename:
253 l.s = UNCONST(char*,N_("not a valid name\n"));
254 rv = su_ERR_INVAL;
255 goto jparse_err;
257 while((c = *++l2.s) != '\0')
258 /* On change adjust `alias' and impl., too */
259 if(!su_cs_is_alnum(c) && c != '_' && c != '-'){
260 if(c == '$' && *l2.s == '\0')
261 break;
262 goto jename;
265 /* Be strict regarding file content */
266 if(UNLIKELY(su_cs_dict_has_key(dp, nsp->s_dat))){
267 l.s = UNCONST(char*,N_("duplicate name"));
268 rv = su_ERR_ADDRINUSE;
269 goto jparse_err;
270 }else{
271 /* Seems to be a usable name. Parse data off */
272 struct n_strlist **tailp;
273 struct mx_name *nphead,*np;
275 nphead = lextract(l2.s = &nsp->s_dat[l2.l + 1], GTO | GFULL |
276 GQUOTE_ENCLOSED_OK);
278 if(UNLIKELY(nphead == NIL)){
279 jeval:
280 n_err(_("*mta_aliases*: %s: ignoring empty/unsupported value: "
281 "%s: %s\n"),
282 n_shexp_quote_cp(masp->mas_path_usr, FAL0),
283 n_shexp_quote_cp(nsp->s_dat, FAL0),
284 n_shexp_quote_cp(l2.s, FAL0));
285 continue;
288 for(np = nphead; np != NIL; np = np->n_flink)
289 /* TODO :include:/file/path directive not yet <> manual! */
290 if((np->n_flags & mx_NAME_ADDRSPEC_ISFILE) &&
291 su_cs_starts_with(np->n_name, ":include:"))
292 goto jeval;
294 UNINIT(tailp, NIL);
295 for(maap = NIL, np = nphead; np != NIL; np = np->n_flink){
296 struct n_strlist *slp;
297 uz i;
299 i = su_cs_len(np->n_fullname) +1;
300 slp = n_STRLIST_ALLOC(i + 1 +
301 (maap == NIL ? 2*Z_ALIGN_OVER(ALIGNOF(*maap)) : 0));
303 if(maap == NIL){
304 maap = P_ALIGN(struct a_mtaali_alias*, maap,
305 &slp->sl_dat[i +1 + 1]);
306 maap->maa_next = NIL;
307 tailp = &maap->maa_values;
308 *maapp = maap;
309 maapp = &maap->maa_next;
312 *tailp = slp;
313 slp->sl_next = NIL;
314 tailp = &slp->sl_next;
315 slp->sl_len = i -1;
316 su_mem_copy(slp->sl_dat, np->n_fullname, i);
318 switch(np->n_flags & mx_NAME_ADDRSPEC_ISMASK){
319 case mx_NAME_ADDRSPEC_ISFILE: c = a_MTAALI_T_FILE; break;
320 case mx_NAME_ADDRSPEC_ISPIPE: c = a_MTAALI_T_PIPE; break;
321 case mx_NAME_ADDRSPEC_ISNAME: c = a_MTAALI_T_NAME; break;
322 default:
323 case mx_NAME_ADDRSPEC_ISADDR: c = a_MTAALI_T_ADDR; break;
325 slp->sl_dat[i] = c;
328 if((rv = su_cs_dict_view_reset_insert(&csdv, nsp->s_dat, maap)
329 ) != su_ERR_NONE){
330 n_err(_("*mta_aliases*: failed to create storage: %s\n"),
331 su_err_doc(rv));
332 goto jdone;
334 maap->maa_key = su_cs_dict_view_key(&csdv);
338 /* Worked last line leftover? */
339 if(l.l == 0){
340 /*sp = n_string_trunc(sp, 0);
341 *break;*/
342 goto jparse_done;
344 nsp = n_string_assign_buf(nsp, l.s, l.l);
346 /* Last line leftover to parse? */
347 if(nsp->s_len > 0){
348 l.l = 0;
349 goto jparse_line;
352 jparse_done:
353 rv = su_ERR_NONE;
354 jdone:
355 mx_fs_linepool_release(line.s, line.l);
357 if(rv != su_ERR_NONE)
358 a_mtaali_gut_csd(dp);
360 mx_fs_close(afp);
361 jleave:
362 NYD_OU;
363 return rv;
365 jparse_err:
366 n_err("*mta-aliases*: %s: %s: %s\n",
367 n_shexp_quote_cp(masp->mas_path_usr, FAL0), V_(l.s),
368 n_shexp_quote_cp(nsp->s_dat, FAL0));
369 goto jdone;
372 static void
373 a_mtaali_expand(uz lvl, char const *name, struct a_mtaali_query *maqp){
374 union {void *v; struct a_mtaali_alias *maa; struct n_strlist *sl;} p;
375 NYD2_IN;
377 ++lvl;
379 if((p.v = su_cs_dict_lookup(maqp->maq_dp, name)) == NIL){
380 jput_name:
381 maqp->maq_err = su_ERR_DESTADDRREQ;
382 maqp->maq_result = cat(nalloc(name, maqp->maq_type | GFULL),
383 maqp->maq_result);
384 }else{
385 for(p.sl = p.maa->maa_values; p.sl != NIL; p.sl = p.sl->sl_next){
386 /* Is it a name itself? */
387 if(p.sl->sl_dat[p.sl->sl_len + 1] == a_MTAALI_T_NAME){
388 if(UCMP(z, lvl, <, n_ALIAS_MAXEXP)) /* TODO not a real error! */
389 a_mtaali_expand(lvl, p.sl->sl_dat, maqp);
390 else{
391 n_err(_("*mta_aliases*: stopping recursion at depth %d\n"),
392 n_ALIAS_MAXEXP);
393 goto jput_name;
395 }else
396 maqp->maq_result = cat(maqp->maq_result,
397 nalloc(p.sl->sl_dat, maqp->maq_type | GFULL));
401 NYD2_OU;
405 c_mtaaliases(void *vp){
406 char const *cp;
407 boole load_only;
408 char **argv;
409 NYD_IN;
411 argv = vp;
412 load_only = FAL0;
414 if((cp = *argv) == NIL)
415 goto jlist;
416 if(argv[1] != NIL)
417 goto jerr;
418 if(su_cs_starts_with_case("show", cp))
419 goto jlist;
420 if(su_cs_starts_with_case("clear", cp))
421 goto jclear;
422 load_only = TRU1;
423 if(su_cs_starts_with_case("load", cp))
424 goto jclear;
425 jerr:
426 mx_cmd_print_synopsis(mx_cmd_firstfit("mtaaliases"), NIL);
427 vp = NIL;
428 jleave:
429 NYD_OU;
430 return (vp == NIL ? n_EXIT_ERR : n_EXIT_OK);
432 jclear:
433 if(a_mtaali_g.mag_path != NIL && a_mtaali_g.mag_path != a_MTAALI_G_ERR){
434 a_mtaali_gut_csd(&a_mtaali_g.mag_dict);
435 su_FREE(a_mtaali_g.mag_path);
437 a_mtaali_g.mag_path = NIL;
438 if(load_only)
439 goto jlist;
440 goto jleave;
442 jlist:{
443 struct n_string quote; /* TODO quoting does not belong; -> RFC 822++ */
444 uz scrwid, l, lw;
445 struct n_strlist *slp;
446 struct a_mtaali_alias *maap;
447 FILE *fp;
449 if((cp = ok_vlook(mta_aliases)) == NIL){
450 n_err(_("mtaaliases: *mta-aliases* not set\n"));
451 vp = NIL;
452 goto jleave;
453 }else if(a_mtaali_cache_init(cp) != su_ERR_NONE ||
454 a_mtaali_g.mag_path == a_MTAALI_G_ERR){
455 n_err(_("mtaaliases: *mta-aliases* had no content\n"));
456 vp = NIL;
457 goto jleave;
459 if(load_only)
460 goto jleave;
462 if((fp = mx_fs_tmp_open("mtaaliases", (mx_FS_O_RDWR | mx_FS_O_UNLINK |
463 mx_FS_O_REGISTER), NIL)) == NIL)
464 fp = n_stdout;
466 n_string_creat_auto(&quote);
467 scrwid = mx_TERMIOS_WIDTH_OF_LISTS();
468 l = 0;
469 for(maap = a_mtaali_g.mag_aliases; maap != NIL; maap = maap->maa_next){
470 boole any;
472 /* Our reader above guarantees the name does not need to be quoted! */
473 fputs(cp = maap->maa_key, fp);
474 putc(':', fp);
475 lw = su_cs_len(cp) + 1;
477 any = FAL0;
478 for(slp = maap->maa_values; slp != NIL; slp = slp->sl_next){
479 uz i;
481 if(!any)
482 any = TRU1;
483 else{
484 putc(',', fp);
485 ++lw;
488 /* TODO Is it a name itself? Otherwise we may need to apply quoting!
489 * TODO quoting does not belong; -> RFC 822++ */
490 cp = slp->sl_dat;
491 i = slp->sl_len;
492 if(cp[i + 1] != a_MTAALI_T_NAME && cp[i + 1] != a_MTAALI_T_ADDR &&
493 su_cs_first_of_cbuf_cbuf(cp, i, " \t\"#:@", 6) != UZ_MAX){
494 char c;
496 n_string_reserve(n_string_trunc(&quote, 0), (slp->sl_len * 2) + 2);
497 n_string_push_c(&quote, '"');
498 while((c = *cp++) != '\0'){
499 if(c == '"' || c == '\\')
500 n_string_push_c(&quote, '\\');
501 n_string_push_c(&quote, c);
503 n_string_push_c(&quote, '"');
504 cp = n_string_cp(&quote);
505 i = quote.s_len;
508 if(lw + i >= scrwid){
509 fputs("\n ", fp);
510 ++l;
511 lw = 2;
513 lw += i + 1;
514 putc(' ', fp);
515 fputs(cp, fp);
517 putc('\n', fp);
518 ++l;
520 /* n_string_gut(&quote); */
522 if(fp != n_stdout){
523 page_or_print(fp, l);
525 mx_fs_close(fp);
526 }else
527 clearerr(fp);
529 goto jleave;
533 mx_mta_aliases_expand(struct mx_name **npp){
534 struct a_mtaali_query maq;
535 struct mx_name *np, *nphead;
536 s32 rv;
537 char const *file;
538 NYD_IN;
540 rv = su_ERR_NONE;
542 /* Is there a possibility that we have to do anything? */
543 if((file = ok_vlook(mta_aliases)) == NIL)
544 goto jleave;
546 for(np = *npp; np != NIL; np = np->n_flink)
547 if(!(np->n_type & GDEL) && (np->n_flags & mx_NAME_ADDRSPEC_ISNAME))
548 break;
549 if(np == NIL)
550 goto jleave;
552 /* Then lookup the cache, creating/updating it first as necessary */
553 if((rv = a_mtaali_cache_init(file)) != su_ERR_NONE ||
554 a_mtaali_g.mag_path == a_MTAALI_G_ERR){
555 rv = su_ERR_DESTADDRREQ;
556 goto jleave;
559 maq.maq_dp = &a_mtaali_g.mag_dict;
560 nphead = *npp;
561 maq.maq_result = *npp = NIL;
562 maq.maq_err = su_ERR_NONE;
564 for(np = nphead; np != NIL; np = np->n_flink){
565 if(np->n_flags & mx_NAME_ADDRSPEC_ISNAME){
566 maq.maq_type = np->n_type;
567 a_mtaali_expand(0, np->n_name, &maq);
568 }else
569 maq.maq_result = cat(maq.maq_result, ndup(np, np->n_type));
572 *npp = maq.maq_result;
573 rv = maq.maq_err;
574 jleave:
575 NYD_OU;
576 return rv;
579 #include "su/code-ou.h"
580 #endif /* mx_HAVE_MTA_ALIASES */
581 /* s-it-mode */