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.
21 #define su_FILE mta_aliases
23 #define mx_SOURCE_MTA_ALIASES
25 #ifndef mx_HAVE_AMALGAMATION
30 #ifdef mx_HAVE_MTA_ALIASES
31 #include <sys/stat.h> /* TODO su_path_info */
34 #include <su/cs-dict.h>
38 #include "mx/file-streams.h"
40 #include "mx/termios.h"
42 #include "mx/mta-aliases.h"
43 #include "su/code-in.h"
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
;
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 */
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
;
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
);
96 a_mtaali_gut_csd(struct su_cs_dict
*csdp
){
97 struct su_cs_dict_view csdv
;
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
;
109 p
.sl
= p
.sl
->sl_next
;
114 su_cs_dict_gut(csdp
);
119 a_mtaali_cache_init(char const *usrfile
){
120 struct a_mtaali_stack mas
;
125 fexpand(mas
.mas_path_usr
= usrfile
, (FEXP_NOPROTO
| FEXP_LOCAL_FILE
|
126 FEXP_NSHELL
))) == NIL
){
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
)
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
;
150 n_err(_("*mta_aliases*: %s: %s\n"),
151 n_shexp_quote_cp(mas
.mas_path_usr
, FAL0
), su_err_doc(rv
));
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
;
162 a_mtaali__read_file(struct a_mtaali_stack
*masp
){
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
;
172 if((afp
= mx_fs_open(masp
->mas_path
, "r")) == NIL
){
174 n_err(_("*mta-aliases*: cannot open %s: %s\n"),
175 n_shexp_quote_cp(masp
->mas_path_usr
, FAL0
), su_err_doc(rv
));
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) */
195 n_str_trim(&l
, n_STR_TRIM_BOTH
);
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] == '#')
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
;
214 nsp
= n_string_push_buf(nsp
, l
.s
, l
.l
);
218 ASSERT(nsp
->s_len
> 0);
221 * An alias definition has the form
222 * name: value1, value2, ...
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!) */
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"));
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.. */
250 c
= *(l2
.s
= nsp
->s_dat
);
251 if(!su_cs_is_alpha(c
) && c
!= '_'){
253 l
.s
= UNCONST(char*,N_("not a valid name\n"));
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')
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
;
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
|
278 if(UNLIKELY(nphead
== NIL
)){
280 n_err(_("*mta_aliases*: %s: ignoring empty/unsupported value: "
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
));
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:"))
295 for(maap
= NIL
, np
= nphead
; np
!= NIL
; np
= np
->n_flink
){
296 struct n_strlist
*slp
;
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));
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
;
309 maapp
= &maap
->maa_next
;
314 tailp
= &slp
->sl_next
;
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;
323 case mx_NAME_ADDRSPEC_ISADDR
: c
= a_MTAALI_T_ADDR
; break;
328 if((rv
= su_cs_dict_view_reset_insert(&csdv
, nsp
->s_dat
, maap
)
330 n_err(_("*mta_aliases*: failed to create storage: %s\n"),
334 maap
->maa_key
= su_cs_dict_view_key(&csdv
);
338 /* Worked last line leftover? */
340 /*sp = n_string_trunc(sp, 0);
344 nsp
= n_string_assign_buf(nsp
, l
.s
, l
.l
);
346 /* Last line leftover to parse? */
355 mx_fs_linepool_release(line
.s
, line
.l
);
357 if(rv
!= su_ERR_NONE
)
358 a_mtaali_gut_csd(dp
);
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
));
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
;
379 if((p
.v
= su_cs_dict_lookup(maqp
->maq_dp
, name
)) == NIL
){
381 maqp
->maq_err
= su_ERR_DESTADDRREQ
;
382 maqp
->maq_result
= cat(nalloc(name
, maqp
->maq_type
| GFULL
),
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
);
391 n_err(_("*mta_aliases*: stopping recursion at depth %d\n"),
396 maqp
->maq_result
= cat(maqp
->maq_result
,
397 nalloc(p
.sl
->sl_dat
, maqp
->maq_type
| GFULL
));
405 c_mtaaliases(void *vp
){
414 if((cp
= *argv
) == NIL
)
418 if(su_cs_starts_with_case("show", cp
))
420 if(su_cs_starts_with_case("clear", cp
))
423 if(su_cs_starts_with_case("load", cp
))
426 mx_cmd_print_synopsis(mx_cmd_firstfit("mtaaliases"), NIL
);
430 return (vp
== NIL
? n_EXIT_ERR
: n_EXIT_OK
);
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
;
443 struct n_string quote
; /* TODO quoting does not belong; -> RFC 822++ */
445 struct n_strlist
*slp
;
446 struct a_mtaali_alias
*maap
;
449 if((cp
= ok_vlook(mta_aliases
)) == NIL
){
450 n_err(_("mtaaliases: *mta-aliases* not set\n"));
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"));
462 if((fp
= mx_fs_tmp_open("mtaaliases", (mx_FS_O_RDWR
| mx_FS_O_UNLINK
|
463 mx_FS_O_REGISTER
), NIL
)) == NIL
)
466 n_string_creat_auto("e
);
467 scrwid
= mx_TERMIOS_WIDTH_OF_LISTS();
469 for(maap
= a_mtaali_g
.mag_aliases
; maap
!= NIL
; maap
= maap
->maa_next
){
472 /* Our reader above guarantees the name does not need to be quoted! */
473 fputs(cp
= maap
->maa_key
, fp
);
475 lw
= su_cs_len(cp
) + 1;
478 for(slp
= maap
->maa_values
; slp
!= NIL
; slp
= slp
->sl_next
){
488 /* TODO Is it a name itself? Otherwise we may need to apply quoting!
489 * TODO quoting does not belong; -> RFC 822++ */
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
){
496 n_string_reserve(n_string_trunc("e
, 0), (slp
->sl_len
* 2) + 2);
497 n_string_push_c("e
, '"');
498 while((c
= *cp
++) != '\0'){
499 if(c
== '"' || c
== '\\')
500 n_string_push_c("e
, '\\');
501 n_string_push_c("e
, c
);
503 n_string_push_c("e
, '"');
504 cp
= n_string_cp("e
);
508 if(lw
+ i
>= scrwid
){
520 /* n_string_gut("e); */
523 page_or_print(fp
, l
);
533 mx_mta_aliases_expand(struct mx_name
**npp
){
534 struct a_mtaali_query maq
;
535 struct mx_name
*np
, *nphead
;
542 /* Is there a possibility that we have to do anything? */
543 if((file
= ok_vlook(mta_aliases
)) == NIL
)
546 for(np
= *npp
; np
!= NIL
; np
= np
->n_flink
)
547 if(!(np
->n_type
& GDEL
) && (np
->n_flags
& mx_NAME_ADDRSPEC_ISNAME
))
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
;
559 maq
.maq_dp
= &a_mtaali_g
.mag_dict
;
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
);
569 maq
.maq_result
= cat(maq
.maq_result
, ndup(np
, np
->n_type
));
572 *npp
= maq
.maq_result
;
579 #include "su/code-ou.h"
580 #endif /* mx_HAVE_MTA_ALIASES */