Commit | Line | Data |
---|---|---|
f8beb284 MW |
1 | #include <sys/types.h> |
2 | #include <pwd.h> | |
3 | #include "strerr.h" | |
4 | #include "stralloc.h" | |
5 | #include "sgetopt.h" | |
6 | #include "substdio.h" | |
7 | #include "error.h" | |
8 | #include "str.h" | |
9 | #include "fmt.h" | |
10 | #include "fork.h" | |
11 | #include "wait.h" | |
12 | #include "readwrite.h" | |
13 | #include "auto_qmail.h" | |
14 | #include "auto_cron.h" | |
15 | #include "errtxt.h" | |
16 | #include "idx.h" | |
17 | ||
18 | #define FATAL "ezmlm-cron: fatal: " | |
19 | ||
20 | void die_usage() | |
21 | { | |
22 | strerr_die2x(100,FATAL, | |
23 | "usage: ezmlm-cron [-cCdDlLvV] [-w dow] [-t hh:mm] [-i hrs] listadr code"); | |
24 | } | |
25 | ||
26 | void die_dow() | |
27 | { | |
28 | strerr_die2x(100,FATAL,ERR_DOW); | |
29 | } | |
30 | ||
31 | void die_nomem() { strerr_die2x(111,FATAL,ERR_NOMEM); } | |
32 | ||
33 | unsigned long deltah = 24L; /* default interval 24h */ | |
34 | unsigned long hh = 4L; /* default time 04:12 */ | |
35 | unsigned long mm = 12L; | |
36 | char *dow = "*"; /* day of week */ | |
25a55efe | 37 | char *qmail_inject = "/usr/sbin/qmail-inject "; |
f8beb284 MW |
38 | char strnum[FMT_ULONG]; |
39 | unsigned long uid,euid; | |
40 | ||
41 | stralloc line = {0}; | |
42 | stralloc rp = {0}; | |
43 | stralloc addr = {0}; | |
44 | stralloc user = {0}; | |
45 | stralloc euser = {0}; | |
46 | stralloc dir = {0}; | |
47 | stralloc listaddr = {0}; | |
48 | ||
49 | struct passwd *ppasswd; | |
50 | ||
51 | int opt,match; | |
52 | int hostmatch; | |
53 | int localmatch; | |
54 | unsigned long dh,t; | |
55 | int founduser = 0; | |
56 | int listmatch = 0; | |
57 | int flagconfig = 0; | |
58 | int flagdelete = 0; | |
59 | int flaglist = 0; | |
60 | int flagdigit = 0; | |
61 | int flagours; | |
62 | int foundlocal; | |
63 | int foundmatch = 0; | |
64 | int nolists = 0; | |
65 | int maxlists; | |
66 | unsigned int pos,pos2,poslocal,len; | |
67 | unsigned int lenhost,lenlocal; | |
68 | unsigned int part0start,part0len; | |
69 | int fdlock,fdin,fdout; | |
70 | ||
71 | char *local = (char *) 0; /* list = local@host */ | |
72 | char *host = (char *) 0; | |
73 | char *code = (char *) 0; /* digest code */ | |
74 | char *cp; | |
75 | ||
76 | void die_syntax() | |
77 | { | |
78 | if (!stralloc_0(&line)) die_nomem(); | |
79 | strerr_die5x(100,FATAL,TXT_EZCRONRC," ",ERR_SYNTAX,line.s); | |
80 | } | |
81 | ||
82 | void die_argument() | |
83 | { | |
84 | strerr_die2x(100,FATAL,ERR_NOT_CLEAN); | |
85 | } | |
86 | ||
87 | int isclean(addr,flagaddr) | |
88 | /* assures that addr has only letters, digits, "-_" */ | |
89 | /* also checks allows single '@' if flagaddr = 1 */ | |
90 | /* returns 1 if clean, 0 otherwise */ | |
91 | char *addr; | |
92 | int flagaddr; /* 1 for addresses with '@', 0 for other args */ | |
93 | { | |
94 | unsigned int pos; | |
95 | register char ch; | |
96 | register char *cp; | |
97 | if (flagaddr) { /* shoud have one '@' */ | |
98 | pos = str_chr(addr,'@'); | |
99 | if (!pos || !addr[pos]) | |
100 | return 0; /* at least 1 char for local */ | |
101 | if (!addr[pos+1]) | |
102 | return 0; /* host must be at least 1 char */ | |
103 | pos++; | |
104 | case_lowerb(addr+pos,str_len(addr)-pos); | |
105 | } else | |
106 | pos = 0; | |
107 | pos += str_chr(addr + pos,'@'); | |
108 | if (addr[pos]) /* but no more */ | |
109 | return 0; | |
110 | cp = addr; | |
111 | while ((ch = *(cp++))) | |
112 | if (!(ch >= 'a' && ch <= 'z') && | |
113 | !(ch >= 'A' && ch <= 'Z') && | |
114 | !(ch >= '0' && ch <= '9') && | |
115 | ch != '.' && ch != '-' && ch != '_' && ch != '@') | |
116 | return 0; | |
117 | return 1; | |
118 | } | |
119 | ||
120 | char inbuf[512]; | |
121 | substdio ssin; | |
122 | ||
123 | char outbuf[512]; | |
124 | substdio ssout; | |
125 | ||
126 | void main(argc,argv) | |
127 | int argc; | |
128 | char **argv; | |
129 | ||
130 | { | |
131 | int child; | |
132 | char *sendargs[4]; | |
133 | int wstat; | |
134 | ||
135 | (void) umask(077); | |
136 | sig_pipeignore(); | |
137 | ||
138 | while ((opt = getopt(argc,argv,"cCdDi:lLt:w:vV")) != opteof) | |
139 | switch (opt) { | |
140 | case 'c': flagconfig = 1; break; | |
141 | case 'C': flagconfig = 0; break; | |
142 | case 'd': flagdelete = 1; break; | |
143 | case 'D': flagdelete = 0; break; | |
144 | case 'i': scan_ulong(optarg,&deltah); break; | |
145 | case 'l': flaglist = 1; break; | |
146 | case 'L': flaglist = 0; break; | |
147 | case 't': | |
148 | pos = scan_ulong(optarg,&hh); | |
149 | if (!optarg[pos++] == ':') die_usage(); | |
150 | pos = scan_ulong(optarg + pos,&mm); | |
151 | break; | |
152 | case 'w': | |
153 | dow = optarg; | |
154 | cp = optarg - 1; | |
155 | while (*(++cp)) { | |
156 | if (*cp >= '0' && *cp <= '7') { | |
157 | if (flagdigit) die_dow(); | |
158 | flagdigit = 1; | |
159 | } else if (*cp == ',') { | |
160 | if (!flagdigit) die_dow(); | |
161 | flagdigit = 0; | |
162 | } else | |
163 | die_dow(); | |
164 | } | |
165 | break; | |
166 | case 'v': | |
167 | case 'V': strerr_die2x(100,"ezmlm-cron version: ",EZIDX_VERSION); | |
168 | default: | |
169 | die_usage(); | |
170 | } | |
171 | if (flaglist + flagdelete + flagconfig > 1) | |
172 | strerr_die2x(100,FATAL,ERR_EXCLUSIVE); | |
173 | uid = getuid(); | |
174 | if (uid && !(euid = geteuid())) | |
175 | strerr_die2x(100,FATAL,ERR_SUID); | |
176 | if (!(ppasswd = getpwuid(uid))) | |
177 | strerr_die2x(100,FATAL,ERR_UID); | |
178 | if (!stralloc_copys(&user,ppasswd->pw_name)) die_nomem(); | |
179 | if (!stralloc_0(&user)) die_nomem(); | |
180 | if (!(ppasswd = getpwuid(euid))) | |
181 | strerr_die2x(100,FATAL,ERR_EUID); | |
182 | if (!stralloc_copys(&dir.s,ppasswd->pw_dir)) die_nomem(); | |
183 | if (!stralloc_0(&dir)) die_nomem(); | |
184 | if (!stralloc_copys(&euser,ppasswd->pw_name)) die_nomem(); | |
185 | if (!stralloc_0(&euser)) die_nomem(); | |
186 | ||
187 | if (chdir(dir.s) == -1) | |
188 | strerr_die4sys(111,FATAL,ERR_SWITCH,dir.s,": "); | |
189 | ||
190 | local = argv[optind++]; /* list address, optional for -c & -l */ | |
191 | if (!local) { | |
192 | if (!flagconfig && !flaglist) | |
193 | die_usage(); | |
194 | lenlocal = 0; | |
195 | lenhost = 0; | |
196 | } else { | |
197 | if (!stralloc_copys(&listaddr,local)) die_nomem(); | |
198 | if (!isclean(local,1)) | |
199 | die_argument(); | |
200 | pos = str_chr(local,'@'); | |
201 | lenlocal = pos; | |
202 | local[pos] = '\0'; | |
203 | host = local + pos + 1; | |
204 | lenhost = str_len(host); | |
205 | code = argv[optind]; | |
206 | if (!code) { /* ignored for -l, -c, and -d */ | |
207 | if (flagdelete || flaglist || flagconfig) | |
208 | /* get away with not putting code for delete */ | |
209 | code = "a"; /* a hack - so what! */ | |
210 | else | |
211 | die_usage(); | |
212 | } else | |
213 | if (!isclean(code,0)) | |
214 | die_argument(); | |
215 | } | |
216 | if ((fdin = open_read(TXT_EZCRONRC)) == -1) | |
217 | strerr_die6sys(111,FATAL,ERR_OPEN,dir.s,"/",TXT_EZCRONRC,": "); | |
218 | /* first line is special */ | |
219 | substdio_fdbuf(&ssin,read,fdin,inbuf,sizeof(inbuf)); | |
220 | if (getln(&ssin,&line,&match,'\n') == -1) | |
221 | strerr_die6sys(111,FATAL,ERR_READ,dir.s,"/",TXT_EZCRONRC,": "); | |
222 | ||
223 | if (!match) | |
224 | strerr_die6sys(111,FATAL,ERR_READ,dir.s,"/",TXT_EZCRONRC,": "); | |
225 | /* (since we have match line.len has to be >= 1) */ | |
226 | line.s[line.len - 1] = '\0'; | |
227 | if (!isclean(line.s,0)) /* host for bounces */ | |
228 | strerr_die4x(100,ERR_CFHOST,dir.s,"/",TXT_EZCRONRC); | |
229 | if (!stralloc_copys(&rp,line.s)) die_nomem(); | |
230 | ||
231 | match = 1; | |
232 | for(;;) { | |
233 | if (!match) break; /* to allow last line without '\n' */ | |
234 | if (getln(&ssin,&line,&match,'\n') == -1) | |
235 | strerr_die6sys(111,FATAL,ERR_READ,dir.s,"/",TXT_EZCRONRC,": "); | |
236 | if (!line.len) | |
237 | break; | |
238 | line.s[line.len-1] = '\0'; | |
239 | if (!case_startb(line.s,line.len,user.s)) | |
240 | continue; | |
241 | pos = user.len - 1; | |
242 | if (pos >= line.len || line.s[pos] != ':') | |
243 | continue; | |
244 | founduser = 1; /* got user line */ | |
245 | break; | |
246 | } | |
247 | close(fdin); | |
248 | if (!founduser) | |
249 | strerr_die2x(100,FATAL,ERR_BADUSER); | |
250 | ||
251 | if (flagconfig) { | |
252 | line.s[line.len-1] = '\n'; /* not very elegant ;-) */ | |
253 | substdio_fdbuf(&ssout,write,1,outbuf,sizeof(outbuf)); | |
254 | if (substdio_put(&ssout,line.s,line.len) == -1) | |
255 | strerr_die3sys(111,FATAL,ERR_WRITE,"stdout: "); | |
256 | if (substdio_flush(&ssout) == -1) | |
257 | strerr_die3sys(111,FATAL,ERR_WRITE,"stdout: "); | |
258 | _exit(0); | |
259 | } | |
260 | ++pos; /* points to first ':' */ | |
261 | len = str_chr(line.s+pos,':'); /* second ':' */ | |
262 | if (!line.s[pos + len]) | |
263 | die_syntax(); | |
264 | if (!local) { /* only -d and std left */ | |
265 | localmatch = 1; | |
266 | hostmatch = 1; | |
267 | } else { | |
268 | hostmatch = 0; | |
269 | if (len <= str_len(local)) | |
270 | if (!str_diffn(line.s+pos,local,len)) | |
271 | localmatch = 1; | |
272 | } | |
273 | pos += len + 1; | |
274 | len = str_chr(line.s + pos,':'); /* third */ | |
275 | if (!line.s[pos + len]) | |
276 | die_syntax(); | |
277 | if (local) { /* check host */ | |
278 | if (len == 0) /* empty host => any host */ | |
279 | hostmatch = 1; | |
280 | else | |
281 | if (len == str_len(host)) | |
282 | if (!case_diffb(line.s+pos,len,host)) | |
283 | hostmatch = 1; | |
284 | } | |
285 | pos += len + 1; | |
286 | pos += scan_ulong(line.s+pos,&maxlists); | |
287 | if (line.s[pos]) { /* check additional lists */ | |
288 | if (line.s[pos] != ':') | |
289 | die_syntax(); | |
290 | if (line.s[pos+1+str_chr(line.s+pos+1,':')]) | |
291 | die_syntax(); /* reminder lists are not separated by ':' */ | |
292 | /* otherwise a ':' or arg miscount will die */ | |
293 | /* silently */ | |
294 | if (local) { | |
295 | while (++pos < line.len) { | |
296 | len = str_chr(line.s + pos,'@'); | |
297 | if (len == lenlocal && !str_diffn(line.s + pos,local,len)) { | |
298 | pos += len; | |
299 | if (!line.s[pos]) break; | |
300 | pos++; | |
301 | len = str_chr(line.s+pos,','); | |
302 | if (len == lenhost && !case_diffb(line.s+pos,len,host)) { | |
303 | listmatch = 1; | |
304 | break; | |
305 | } | |
306 | } | |
307 | pos += len; | |
308 | } | |
309 | } | |
310 | } | |
311 | if (!listmatch) { | |
312 | if (!hostmatch) | |
313 | strerr_die2x(100,FATAL,ERR_BADHOST); | |
314 | if (!localmatch) | |
315 | strerr_die2x(100,FATAL,ERR_BADLOCAL); | |
316 | } | |
317 | /* assemble correct line */ | |
318 | if (!flaglist) { | |
319 | if (!stralloc_copyb(&addr,strnum,fmt_ulong(strnum,mm))) die_nomem(); | |
320 | if (!stralloc_cats(&addr," ")) die_nomem(); | |
321 | dh = 0L; | |
322 | if (deltah <= 3L) dh = deltah; | |
323 | else if (deltah <= 6L) dh = 6L; | |
324 | else if (deltah <= 12L) dh = 12L; | |
325 | else if (deltah <= 24L) dh = 24L; | |
326 | else if (deltah <= 48L) { | |
327 | if (dow[0] == '*') dow = "1,3,5"; | |
328 | } else if (deltah <= 72L) { | |
329 | if (dow[0] == '*') dow = "1,4"; | |
330 | } else | |
331 | if (dow[0] == '*') dow = "1"; | |
332 | ||
333 | if (!dh) { | |
334 | if (!stralloc_cats(&addr,"*")) die_nomem(); | |
335 | } else { | |
336 | if (!stralloc_catb(&addr,strnum,fmt_ulong(strnum,hh))) die_nomem(); | |
337 | for (t = hh + dh; t < hh + 24L; t+=dh) { | |
338 | if (!stralloc_cats(&addr,",")) die_nomem(); | |
339 | if (!stralloc_catb(&addr,strnum,fmt_ulong(strnum,t % 24L))) die_nomem(); | |
340 | } | |
341 | } | |
342 | if (!stralloc_cats(&addr," * * ")) die_nomem(); | |
343 | if (!stralloc_cats(&addr,dow)) die_nomem(); | |
344 | if (!stralloc_cats(&addr," ")) die_nomem(); | |
345 | part0start = addr.len; /* /var/qmail/bin/qmail-inject */ | |
f8beb284 MW |
346 | if (!stralloc_cats(&addr,qmail_inject)) die_nomem(); |
347 | part0len = addr.len - part0start; | |
348 | if (!stralloc_cats(&addr,local)) die_nomem(); | |
349 | if (!stralloc_cats(&addr,"-dig-")) die_nomem(); | |
350 | if (!stralloc_cats(&addr,code)) die_nomem(); | |
351 | if (!stralloc_cats(&addr,"@")) die_nomem(); | |
352 | if (!stralloc_cats(&addr,host)) die_nomem(); | |
353 | /* feed 'Return-Path: <user@host>' to qmail-inject */ | |
354 | if (!stralloc_cats(&addr,"%Return-path: <")) die_nomem(); | |
355 | if (!stralloc_cats(&addr,user.s)) die_nomem(); | |
356 | if (!stralloc_cats(&addr,"@")) die_nomem(); | |
357 | if (!stralloc_cat(&addr,&rp)) die_nomem(); | |
358 | if (!stralloc_cats(&addr,">\n")) die_nomem(); | |
359 | } | |
360 | if (!stralloc_0(&addr)) die_nomem(); | |
361 | ||
362 | if (!flaglist) { | |
363 | /* now to rewrite crontab we need to lock */ | |
364 | fdlock = open_append("crontabl"); | |
365 | if (fdlock == -1) | |
366 | strerr_die4sys(111,FATAL,ERR_OPEN,dir.s,"/crontabl: "); | |
367 | if (lock_ex(fdlock) == -1) { | |
368 | close(fdlock); | |
369 | strerr_die4sys(111,FATAL,ERR_OBTAIN,dir.s,"/crontabl: "); | |
370 | } | |
371 | } /* if !flaglist */ | |
372 | if ((fdin = open_read("crontab")) == -1) { | |
373 | if (errno != error_noent) | |
374 | strerr_die4sys(111,FATAL,ERR_READ,dir.s,"/crontab: "); | |
375 | } else | |
376 | substdio_fdbuf(&ssin,read,fdin,inbuf,sizeof(inbuf)); | |
377 | if (flaglist) | |
378 | substdio_fdbuf(&ssout,write,1,outbuf,sizeof(outbuf)); | |
379 | else { | |
380 | if ((fdout = open_trunc("crontabn")) == -1) | |
381 | strerr_die4sys(111,FATAL,ERR_WRITE,dir.s,"/crontabn: "); | |
382 | substdio_fdbuf(&ssout,write,fdout,outbuf,sizeof(outbuf)); | |
383 | } | |
384 | line.len = 0; | |
385 | ||
386 | if (fdin != -1) { | |
387 | for (;;) { | |
388 | if (!flaglist && line.len) { | |
389 | line.s[line.len-1] = '\n'; | |
390 | if (substdio_put(&ssout,line.s,line.len) == -1) | |
391 | strerr_die4sys(111,FATAL,ERR_WRITE,dir.s,"/crontabn: "); | |
392 | } | |
393 | if (getln(&ssin,&line,&match,'\n') == -1) | |
394 | strerr_die4sys(111,FATAL,ERR_READ,dir.s,"/crontab: "); | |
395 | if (!match) | |
396 | break; | |
397 | flagours = 0; /* assume entry is not ours */ | |
398 | foundlocal = 0; | |
399 | line.s[line.len - 1] = '\0'; /* match so at least 1 char */ | |
400 | pos = 0; | |
401 | while (line.s[pos] == ' ' && line.s[pos] == '\t') ++pos; | |
402 | if (line.s[pos] == '#') | |
403 | continue; /* cron comment */ | |
404 | pos = str_chr(line.s,'/'); | |
f8beb284 MW |
405 | if (!str_start(line.s+pos,qmail_inject)) continue; |
406 | pos += str_len(qmail_inject); | |
407 | poslocal = pos; | |
408 | pos = byte_rchr(line.s,line.len,'<'); /* should be Return-Path: < */ | |
409 | if (pos == line.len) | |
410 | continue; /* not ezmlm-cron line */ | |
411 | pos++; | |
412 | len = str_chr(line.s+pos,'@'); | |
413 | if (len == user.len - 1 && !str_diffn(line.s+pos,user.s,len)) { | |
414 | flagours = 1; | |
415 | ++nolists; /* belongs to this user */ | |
416 | } | |
417 | if (!local) { | |
418 | foundlocal = 1; | |
419 | } else { | |
420 | pos = poslocal + str_chr(line.s+poslocal,'@'); | |
421 | if (pos + lenhost +1 >= line.len) continue; | |
422 | if (case_diffb(line.s+pos+1,lenhost,host)) continue; | |
423 | if (line.s[pos+lenhost+1] != '%') continue; | |
424 | /* check local */ | |
425 | if (poslocal + lenlocal + 5 >= line.len) continue; | |
426 | if (!str_start(line.s+poslocal,local)) continue; | |
427 | pos2 = poslocal+lenlocal; | |
428 | if (!str_start(line.s+pos2,"-dig-")) continue; | |
429 | foundlocal = 1; | |
430 | } | |
431 | if (foundlocal) { | |
432 | foundmatch = 1; | |
433 | if (flaglist && (local || flagours)) { | |
434 | if (substdio_put(&ssout,line.s,line.len) == -1) | |
435 | strerr_die3sys(111,FATAL,ERR_WRITE,"stdout: "); | |
436 | if (substdio_put(&ssout,"\n",1) == -1) | |
437 | strerr_die3sys(111,FATAL,ERR_WRITE,"stdout: "); | |
438 | } | |
439 | line.len = 0; /* same - kill line */ | |
440 | if (flagours) | |
441 | --nolists; | |
442 | } | |
443 | } | |
444 | close(fdin); | |
445 | } | |
446 | if (flaglist) { | |
447 | if (substdio_flush(&ssout) == -1) | |
448 | strerr_die3sys(111,FATAL,ERR_FLUSH,"stdout: "); | |
449 | if (foundmatch) /* means we had a match */ | |
450 | _exit(0); | |
451 | else | |
452 | strerr_die2x(100,FATAL,ERR_NO_MATCH); | |
453 | } | |
454 | /* only -d and regular use left */ | |
455 | ||
456 | if (nolists >= maxlists && !flagdelete) | |
457 | strerr_die2x(100,FATAL,ERR_LISTNO); | |
458 | if (!flagdelete) | |
459 | if (substdio_put(&ssout,addr.s,addr.len-1) == -1) | |
460 | strerr_die4sys(111,FATAL,ERR_WRITE,dir.s,"/crontabn: "); | |
461 | if (flagdelete && !foundlocal) | |
462 | strerr_die2x(111,FATAL,ERR_NO_MATCH); | |
463 | if (substdio_flush(&ssout) == -1) | |
464 | strerr_die4sys(111,FATAL,ERR_FLUSH,dir.s,"/crontabn: "); | |
465 | if (fsync(fdout) == -1) | |
466 | strerr_die4sys(111,FATAL,ERR_SYNC,dir.s,"/crontabn++: "); | |
467 | if (close(fdout) == -1) | |
468 | strerr_die4sys(111,FATAL,ERR_CLOSE,dir.s,"/crontabn: "); | |
469 | if (rename("crontabn","crontab") == -1) | |
470 | strerr_die4sys(111,FATAL,ERR_MOVE,dir.s,"/crontabn: "); | |
471 | sendargs[0] = "sh"; | |
472 | sendargs[1] = "-c"; | |
473 | ||
474 | if (!stralloc_copys(&line,auto_cron)) die_nomem(); | |
475 | if (!stralloc_cats(&line,"/crontab '")) die_nomem(); | |
476 | if (!stralloc_cats(&line,dir.s)) die_nomem(); | |
477 | if (!stralloc_cats(&line,"/crontab'")) die_nomem(); | |
478 | if (!stralloc_0(&line)) die_nomem(); | |
479 | sendargs[2] = line.s; | |
480 | sendargs[3] = 0; | |
481 | switch(child = fork()) { | |
482 | case -1: | |
483 | strerr_die2sys(111,FATAL,ERR_FORK); | |
484 | case 0: | |
485 | if (setreuid(euid,euid) == -1) | |
486 | strerr_die2sys(100,FATAL,ERR_SETUID); | |
487 | execvp(*sendargs,sendargs); | |
488 | if (errno == error_txtbsy || errno == error_nomem || | |
489 | errno == error_io) | |
490 | strerr_die4sys(111,FATAL,ERR_EXECUTE,sendargs[2],": "); | |
491 | else | |
492 | strerr_die4sys(100,FATAL,ERR_EXECUTE,sendargs[2],": "); | |
493 | } | |
494 | /* parent */ | |
495 | wait_pid(&wstat,child); | |
496 | if (wait_crashed(wstat)) | |
497 | strerr_die2x(111,FATAL,ERR_CHILD_CRASHED); | |
498 | switch(wait_exitcode(wstat)) { | |
499 | case 0: | |
500 | _exit(0); | |
501 | default: | |
502 | strerr_die2x(111,FATAL,ERR_CRONTAB); | |
503 | } | |
504 | } |