1- // SPDX-License-Identifier: GPL-2 .0-only WITH linking exception
2- // SPDX-FileCopyrightText: 2020– 2025 grommunio GmbH
1+ // SPDX-License-Identifier: AGPL-3 .0-or-later
2+ // SPDX-FileCopyrightText: 2025 grommunio GmbH
33// This file is part of Gromox.
44#include < cstdint>
5- #include < cstdio>
65#include < cstring>
7- #include < ctime>
8- #include < fcntl.h>
9- #include < unistd.h>
106#include < libHX/string.h>
11- #include < sys/stat.h>
12- #include < sys/types.h>
13- #include < sys/wait.h>
14- #include < gromox/config_file.hpp>
15- #include < gromox/defs.h>
7+ #include < vmime/addressList.hpp>
8+ #include < vmime/dateTime.hpp>
9+ #include < vmime/message.hpp>
10+ #include < vmime/stringContentHandler.hpp>
1611#include < gromox/exmdb_client.hpp>
1712#include < gromox/exmdb_rpc.hpp>
1813#include < gromox/hook_common.h>
19- #include < gromox/mail_func.hpp>
2014#include < gromox/mysql_adaptor.hpp>
15+ #include < gromox/oxcmail.hpp>
2116#include < gromox/util.hpp>
2217#include " exmdb_local.hpp"
2318
@@ -26,186 +21,111 @@ DECLARE_HOOK_API(exmdb_local, extern);
2621using namespace exmdb_local ;
2722unsigned int autoreply_silence_window;
2823
24+ static int is_same_org (const char *a, const char *b)
25+ {
26+ a = strchr (a, ' @' );
27+ b = strchr (b, ' @' );
28+ if (a == nullptr || b == nullptr )
29+ return -1 ;
30+ ++a; ++b;
31+ return strcasecmp (a, b) == 0 ? 1 : mysql_adaptor_check_same_org2 (a, b);
32+ }
33+
34+ /* *
35+ * @user_home: Maildir for basically @rcpt
36+ * @from: Sender for the autoresponse (Original Envelope-To)
37+ * @rcpt: Recipient for the autoresponse (Original Envelope-From)
38+ */
2939void auto_response_reply (const char *user_home,
3040 const char *from, const char *rcpt) try
3141{
32- BOOL b_found;
33- char *pcontent;
34- BOOL b_internal;
35- char charset[32 ]{};
36- struct tm tm_buff;
37- int i, j, fd, len;
38- char subject[1024 ];
39- char buff[64 *1024 ];
40- char date_buff[128 ];
41- char temp_path[256 ];
42- MIME_FIELD mime_field;
43- struct stat node_stat;
44- char content_type[256 ];
45- char template_path[256 ];
46- char new_buff[128 *1024 ];
47- MESSAGE_CONTEXT *pcontext;
48-
49- if (strcasecmp (from, rcpt) == 0 || strcasecmp (rcpt, ENVELOPE_RCPT_NULL) == 0 )
50- return ;
51- auto ptoken = strchr (from, ' @' );
52- auto ptoken1 = strchr (rcpt, ' @' );
53- if (ptoken == nullptr || ptoken1 == nullptr )
42+ auto same_org = is_same_org (from, rcpt);
43+ if (same_org < 0 )
5444 return ;
5545
56- if (0 == strcasecmp (ptoken, ptoken1)) {
57- b_internal = TRUE ;
58- } else {
59- auto lcldom = mysql_adaptor_domain_list_query (ptoken + 1 );
60- if (lcldom < 0 ) {
61- mlog (LV_ERR, " auto_response: domain_list_query: %s" ,
62- strerror (-lcldom));
63- return ;
64- }
65- b_internal = lcldom < 1 ? false :
66- mysql_adaptor_check_same_org2 (ptoken + 1 , ptoken1 + 1 );
67- }
68-
69- snprintf (temp_path, 256 , " %s/config/autoreply.cfg" , user_home);
70- auto pconfig = config_file_init (temp_path, nullptr );
71- if (pconfig == nullptr )
72- return ;
73- auto str_value = pconfig->get_value (" OOF_STATE" );
74- if (str_value == nullptr )
46+ static constexpr proptag_t tags_1[] = {PR_OOF_STATE};
47+ static constexpr PROPTAG_ARRAY tags_1r = {std::size (tags_1), deconst (tags_1)};
48+ TPROPVAL_ARRAY store_props;
49+ if (!exmdb_client->get_store_properties (user_home, CP_UTF8, &tags_1r, &store_props)) {
50+ mlog (LV_ERR, " get_store_properties %s failed" , user_home);
7551 return ;
76- uint8_t reply_state = strtol (str_value, nullptr , 0 );
77- if (reply_state != 1 && reply_state != 2 )
78- return ;
79- auto cur_time = time (nullptr );
80- if (2 == reply_state) {
81- str_value = pconfig->get_value (" START_TIME" );
82- if (str_value != nullptr && strtoll (str_value, nullptr , 0 ) > cur_time)
83- return ;
84- str_value = pconfig->get_value (" END_TIME" );
85- if (str_value != nullptr && cur_time > strtoll (str_value, nullptr , 0 ))
86- return ;
87- }
88- if (b_internal) {
89- snprintf (template_path, 256 , " %s/config/internal-reply" , user_home);
90- } else {
91- str_value = pconfig->get_value (" ALLOW_EXTERNAL_OOF" );
92- if (str_value == nullptr || strtol (str_value, nullptr , 0 ) == 0 )
93- return ;
94- str_value = pconfig->get_value (" EXTERNAL_AUDIENCE" );
95- if (str_value != nullptr && strtol (str_value, nullptr , 0 ) != 0 ) {
96- if (!exmdb_client_remote::check_contact_address (user_home, rcpt,
97- &b_found) || !b_found)
98- return ;
99- }
100- snprintf (template_path, 256 , " %s/config/external-reply" , user_home);
10152 }
53+ auto bval = store_props.get <const uint8_t >(PR_OOF_STATE);
54+ if (bval == nullptr || *bval == 0 )
55+ return ;
10256
57+ /* Elvis has left the building */
10358 uint64_t tdiff;
10459 if (exmdb_client->autoreply_tsquery (user_home, rcpt,
10560 autoreply_silence_window, &tdiff) && tdiff < autoreply_silence_window)
10661 /* Autoreply already sent */
10762 return ;
108- fd = open (template_path, O_RDONLY);
109- if (fd < 0 )
110- return ;
111- if (fstat (fd, &node_stat) != 0 || node_stat.st_size == 0 ||
112- static_cast <unsigned long long >(node_stat.st_size ) > sizeof (buff) - 1 ||
113- read (fd, buff, node_stat.st_size ) != node_stat.st_size ) {
114- close (fd);
63+
64+ static constexpr proptag_t tags_2[] = {
65+ PR_EC_OUTOFOFFICE_SUBJECT, PR_EC_OUTOFOFFICE_MSG,
66+ PR_EC_ALLOW_EXTERNAL, PR_EC_EXTERNAL_AUDIENCE,
67+ PR_EC_EXTERNAL_SUBJECT, PR_EC_EXTERNAL_REPLY,
68+ };
69+ static constexpr PROPTAG_ARRAY tags_2r = {std::size (tags_2), deconst (tags_2)};
70+ TPROPVAL_ARRAY ar_props;
71+ if (!exmdb_client->autoreply_getprop (user_home, CP_UTF8,
72+ &tags_2r, &ar_props)) {
73+ mlog (LV_ERR, " autoreply_getprop %s failed" , user_home);
11574 return ;
11675 }
117- close (fd);
11876
119- if (' \n ' == buff[0 ]) {
120- new_buff[0 ] = ' \r ' ;
121- new_buff[1 ] = ' \n ' ;
122- i = 1 ;
123- j = 2 ;
124- } else {
125- new_buff[0 ] = buff[0 ];
126- i = 1 ;
127- j = 1 ;
128- }
129- for (; i<node_stat.st_size ; i++, j++) {
130- if (buff[i] == ' \n ' && buff[i-1 ] != ' \r ' )
131- new_buff[j++] = ' \r ' ;
132- new_buff[j] = buff[i];
77+ if (!same_org) {
78+ bval = ar_props.get <const uint8_t >(PR_EC_ALLOW_EXTERNAL);
79+ if (bval == nullptr || *bval == 0 )
80+ return ;
81+ // Note: counterintuitive but intentional: known (contacts_only) -> 1, all_audiences -> 0
82+ bval = ar_props.get <const uint8_t >(PR_EC_EXTERNAL_AUDIENCE);
83+ if (bval != nullptr && *bval == 0 ) {
84+ BOOL b_found = false ;
85+ if (!exmdb_client_remote::check_contact_address (user_home, rcpt,
86+ &b_found) || !b_found)
87+ return ;
88+ }
13389 }
134- new_buff[j] = ' \0 ' ;
13590
91+ auto subject_text = znul (ar_props.get <const char >(same_org ? PR_EC_OUTOFOFFICE_SUBJECT : PR_EC_EXTERNAL_SUBJECT));
92+ auto message_text = znul (ar_props.get <const char >(same_org ? PR_EC_OUTOFOFFICE_MSG : PR_EC_EXTERNAL_REPLY));
93+ vmime::parsingContext vpctx;
94+ vpctx.setInternationalizedEmailSupport (true ); /* RFC 6532 */
95+ vmime::message vmsg;
13696
137- i = 0 ;
138- pcontent = NULL ;
139- strcpy (content_type, " text/plain" );
140- strcpy (subject, " auto response message" );
141- while (i < j) {
142- auto parsed_length = parse_mime_field (new_buff + i, j - i, &mime_field);
143- i += parsed_length;
144- if (parsed_length == 0 )
145- return ;
146- if (strcasecmp (mime_field.name .c_str (), " Content-Type" ) == 0 ) {
147- gx_strlcpy (content_type, mime_field.value .c_str (), std::size (content_type));
148- charset[0 ] = ' \0 ' ;
149- auto ptoken2 = strchr (content_type, ' ;' );
150- if (ptoken2 != nullptr ) {
151- *ptoken2 = ' \0 ' ;
152- ++ptoken2;
153- ptoken2 = strcasestr (ptoken2, " charset=" );
154- if (ptoken2 != nullptr ) {
155- gx_strlcpy (charset, &ptoken2[8 ], std::size (charset));
156- ptoken2 = strchr (charset, ' ;' );
157- if (ptoken2 != nullptr )
158- *ptoken2 = ' \0 ' ;
159- HX_strrtrim (charset);
160- HX_strltrim (charset);
161- len = strlen (charset);
162- if (' "' == charset[len - 1 ]) {
163- len --;
164- charset[len] = ' \0 ' ;
165- }
166- if (*charset == ' "' )
167- memmove (charset, charset + 1 , len);
168- }
169- }
170- } else if (strcasecmp (mime_field.name .c_str (), " Subject" ) == 0 ) {
171- gx_strlcpy (subject, mime_field.value .c_str (), std::size (subject));
172- }
173- if (' \r ' == new_buff[i] && ' \n ' == new_buff[i + 1 ]) {
174- pcontent = new_buff + i + 2 ;
175- break ;
176- }
97+ auto hdr = vmsg.getHeader ();
98+ hdr->getField (" MIME-Version" )->setValue (" 1.0" );
99+ hdr->From ()->setValue (from);
100+ {
101+ vmime::addressList target_list;
102+ vmime::mailbox target;
103+ target.setEmail (rcpt);
104+ target_list.appendAddress (vmime::make_shared<vmime::mailbox>(target));
105+ hdr->To ()->setValue (target_list);
177106 }
178- if (pcontent == nullptr )
179- return ;
180- pcontext = get_context ();
181- if (pcontext == nullptr )
182- return ;
183- gx_strlcpy (pcontext->ctrl .from , from, std::size (pcontext->ctrl .from ));
184- pcontext->ctrl .rcpt .emplace_back (rcpt);
185- auto pmime = pcontext->mail .add_head ();
186- if (NULL == pmime) {
187- put_context (pcontext);
107+ hdr->getField (" X-Auto-Response-Suppress" )->setValue (" All" );
108+ hdr->Date ()->setValue (vmime::datetime::now ());
109+ hdr->Subject ()->setValue (vmime::text (subject_text, vmime::charsets::UTF_8));
110+
111+ vmime::encoding enc;
112+ enc.setUsage (vmime::encoding::EncodingUsage::USAGE_TEXT);
113+ vmsg.getBody ()->setContents (vmime::make_shared<vmime::stringContentHandler>(message_text, std::move (enc)),
114+ vmime::mediaType (vmime::mediaTypes::TEXT, vmime::mediaTypes::TEXT_HTML),
115+ vmime::charsets::UTF_8);
116+
117+ auto ctx = get_context ();
118+ if (ctx == nullptr )
188119 return ;
189- }
190- if (!pmime->set_content_type (content_type))
191- mlog (LV_WARN, " W-1777: set_content_type unsuccessful" );
192- if (*charset != ' \0 ' )
193- pmime->set_content_param (" charset" , charset);
194- pmime->set_field (" From" , from);
195- pmime->set_field (" To" , rcpt);
196- pmime->set_field (" MIME-Version" , " 1.0" );
197- pmime->set_field (" X-Auto-Response-Suppress" , " All" );
198- strftime (date_buff, 128 , " %a, %d %b %Y %H:%M:%S %z" ,
199- localtime_r (&cur_time, &tm_buff));
200- pmime->set_field (" Date" , date_buff);
201- pmime->set_field (" Subject" , subject);
202- if (!pmime->write_content (pcontent,
203- new_buff + j - pcontent, mime_encoding::automatic)) {
204- put_context (pcontext);
120+ gx_strlcpy (ctx->ctrl .from , from, std::size (ctx->ctrl .from ));
121+ ctx->ctrl .rcpt .emplace_back (rcpt);
122+ if (!vmail_to_mail (vmsg, ctx->mail )) {
123+ mlog (LV_ERR, " %s: vmail_to_mail failed" , __func__);
124+ put_context (ctx);
205125 return ;
206126 }
207- enqueue_context (pcontext );
127+ enqueue_context (ctx );
208128 exmdb_client->autoreply_tsupdate (user_home, rcpt);
209129} catch (const std::bad_alloc &) {
210- mlog (LV_ERR, " E-1081 : ENOMEM" );
130+ mlog (LV_ERR, " %s : ENOMEM" , __func__ );
211131}
0 commit comments