Add lock tracing
[gpgol.git] / src / wks-helper.cpp
1 /* wks-helper.cpp - Web Key Services for GpgOL
2  * Copyright (C) 2018 Intevation GmbH
3  *
4  * This file is part of GpgOL.
5  *
6  * GpgOL is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 2.1 of the License, or (at your option) any later version.
10  *
11  * GpgOL is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  * GNU Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with this program; if not, see <http://www.gnu.org/licenses/>.
18  */
19
20 #include "wks-helper.h"
21
22 #include "common.h"
23 #include "cpphelp.h"
24 #include "oomhelp.h"
25 #include "windowmessages.h"
26 #include "mail.h"
27 #include "mapihelp.h"
28
29 #include <map>
30 #include <sstream>
31
32 #include <unistd.h>
33 #include <stdlib.h>
34
35 #include <gpg-error.h>
36 #include <gpgme++/key.h>
37 #include <gpgme++/data.h>
38 #include <gpgme++/context.h>
39
40 #define CHECK_MIN_INTERVAL (60 * 60 * 24 * 7)
41 #define WKS_REG_KEY "webkey"
42
43 #undef _
44 #define _(a) utf8_gettext (a)
45
46 static std::map <std::string, WKSHelper::WKSState> s_states;
47 static std::map <std::string, time_t> s_last_checked;
48 static std::map <std::string, std::pair <GpgME::Data *, Mail *> > s_confirmation_cache;
49
50 static WKSHelper* singleton = nullptr;
51
52 GPGRT_LOCK_DEFINE (wks_lock);
53
54 WKSHelper::WKSHelper()
55 {
56   load ();
57 }
58
59 WKSHelper::~WKSHelper ()
60 {
61   // Ensure that we are not destroyed while
62   // worker is running.
63   gpgol_lock (&wks_lock);
64   gpgol_unlock (&wks_lock);
65 }
66
67 const WKSHelper*
68 WKSHelper::instance ()
69 {
70   if (!singleton)
71     {
72       singleton = new WKSHelper ();
73     }
74   return singleton;
75 }
76
77 WKSHelper::WKSState
78 WKSHelper::get_state (const std::string &mbox) const
79 {
80   gpgol_lock (&wks_lock);
81   const auto it = s_states.find(mbox);
82   const auto dataEnd = s_states.end();
83   gpgol_unlock (&wks_lock);
84   if (it == dataEnd)
85     {
86       return NotChecked;
87     }
88   return it->second;
89 }
90
91 time_t
92 WKSHelper::get_check_time (const std::string &mbox) const
93 {
94   gpgol_lock (&wks_lock);
95   const auto it = s_last_checked.find(mbox);
96   const auto dataEnd = s_last_checked.end();
97   gpgol_unlock (&wks_lock);
98   if (it == dataEnd)
99     {
100       return 0;
101     }
102   return it->second;
103 }
104
105 std::pair <GpgME::Data *, Mail *>
106 WKSHelper::get_cached_confirmation (const std::string &mbox) const
107 {
108   gpgol_lock (&wks_lock);
109   const auto it = s_confirmation_cache.find(mbox);
110   const auto dataEnd = s_confirmation_cache.end();
111
112   if (it == dataEnd)
113     {
114       gpgol_unlock (&wks_lock);
115       return std::make_pair (nullptr, nullptr);
116     }
117   auto ret = it->second;
118   s_confirmation_cache.erase (it);
119   gpgol_unlock (&wks_lock);
120   return ret;
121 }
122
123 static std::string
124 get_wks_client_path ()
125 {
126   char *gpg4win_dir = get_gpg4win_dir ();
127   if (!gpg4win_dir)
128     {
129       TRACEPOINT;
130       return std::string ();
131     }
132   const auto ret = std::string (gpg4win_dir) +
133                   "\\..\\GnuPG\\bin\\gpg-wks-client.exe";
134   xfree (gpg4win_dir);
135
136   if (!access (ret.c_str (), F_OK))
137     {
138       return ret;
139     }
140   log_debug ("%s:%s: Failed to find wks-client in '%s'",
141              SRCNAME, __func__, ret.c_str ());
142   return std::string ();
143 }
144
145 static bool
146 check_published (const std::string &mbox)
147 {
148   const auto wksPath = get_wks_client_path ();
149
150   if (wksPath.empty())
151     {
152       return 0;
153     }
154
155   std::vector<std::string> args;
156
157   args.push_back (wksPath);
158   args.push_back (std::string ("--status-fd"));
159   args.push_back (std::string ("1"));
160   args.push_back (std::string ("--check"));
161   args.push_back (mbox);
162
163   // Spawn the process
164   auto ctx = GpgME::Context::createForEngine (GpgME::SpawnEngine);
165
166   if (!ctx)
167     {
168       TRACEPOINT;
169       return false;
170     }
171
172   GpgME::Data mystdin, mystdout, mystderr;
173
174   char **cargs = vector_to_cArray (args);
175
176   GpgME::Error err = ctx->spawn (cargs[0], const_cast <const char **> (cargs),
177                                  mystdin, mystdout, mystderr,
178                                  GpgME::Context::SpawnNone);
179   release_cArray (cargs);
180
181   if (err)
182     {
183       log_debug ("%s:%s: WKS client spawn code: %i asString: %s",
184                  SRCNAME, __func__, err.code(), err.asString());
185       return false;
186     }
187   auto data = mystdout.toString ();
188   rtrim (data);
189
190   return data == "[GNUPG:] SUCCESS";
191 }
192
193 static DWORD WINAPI
194 do_check (LPVOID arg)
195 {
196   const auto wksPath = get_wks_client_path ();
197
198   if (wksPath.empty())
199     {
200       return 0;
201     }
202
203   std::vector<std::string> args;
204   const auto mbox = std::string ((char *) arg);
205   xfree (arg);
206
207   args.push_back (wksPath);
208   args.push_back (std::string ("--status-fd"));
209   args.push_back (std::string ("1"));
210   args.push_back (std::string ("--supported"));
211   args.push_back (mbox);
212
213   // Spawn the process
214   auto ctx = GpgME::Context::createForEngine (GpgME::SpawnEngine);
215
216   if (!ctx)
217     {
218       TRACEPOINT;
219       return 0;
220     }
221
222   GpgME::Data mystdin, mystdout, mystderr;
223
224   char **cargs = vector_to_cArray (args);
225
226   GpgME::Error err = ctx->spawn (cargs[0], const_cast <const char **> (cargs),
227                                  mystdin, mystdout, mystderr,
228                                  GpgME::Context::SpawnNone);
229   release_cArray (cargs);
230
231   if (err)
232     {
233       log_debug ("%s:%s: WKS client spawn code: %i asString: %s",
234                  SRCNAME, __func__, err.code(), err.asString());
235       return 0;
236     }
237
238   auto data = mystdout.toString ();
239   rtrim (data);
240
241   bool success = data == "[GNUPG:] SUCCESS";
242   // TODO Figure out NeedsPublish state.
243   auto state = success ? WKSHelper::NeedsPublish : WKSHelper::NotSupported;
244   bool isPublished = false;
245
246   if (success)
247     {
248       log_debug ("%s:%s: WKS client: '%s' is supported",
249                  SRCNAME, __func__, anonstr (mbox.c_str ()));
250       isPublished = check_published (mbox);
251     }
252
253   if (isPublished)
254     {
255       log_debug ("%s:%s: WKS client: '%s' is published",
256                  SRCNAME, __func__, anonstr (mbox.c_str ()));
257       state = WKSHelper::IsPublished;
258     }
259
260   WKSHelper::instance()->update_state (mbox, state, false);
261   WKSHelper::instance()->update_last_checked (mbox, time (0));
262
263   return 0;
264 }
265
266
267 void
268 WKSHelper::start_check (const std::string &mbox, bool forced) const
269 {
270   const auto state = get_state (mbox);
271
272   if (!forced && (state != NotChecked && state != NotSupported))
273     {
274       log_debug ("%s:%s: Check aborted because its neither "
275                  "not supported nor not checked.",
276                  SRCNAME, __func__);
277       return;
278     }
279
280   auto lastTime = get_check_time (mbox);
281   auto now = time (0);
282
283   if (!forced && (state == NotSupported && lastTime &&
284                   difftime (now, lastTime) < CHECK_MIN_INTERVAL))
285     {
286       /* Data is new enough */
287       log_debug ("%s:%s: Check aborted because last checked is too recent.",
288                  SRCNAME, __func__);
289       return;
290     }
291
292   if (mbox.empty())
293     {
294       log_debug ("%s:%s: start check called without mbox",
295                  SRCNAME, __func__);
296     }
297
298   log_debug ("%s:%s: WKSHelper starting check",
299              SRCNAME, __func__);
300   /* Start the actual work that can be done in a background thread. */
301   CloseHandle (CreateThread (nullptr, 0, do_check, xstrdup (mbox.c_str ()), 0,
302                              nullptr));
303   return;
304 }
305
306 void
307 WKSHelper::load () const
308 {
309   /* Map of mbox'es to states. states are <state>;<last_checked> */
310   const auto map = get_registry_subkeys (WKS_REG_KEY);
311
312   for (const auto &pair: map)
313     {
314       const auto mbox = pair.first;
315       const auto states = gpgol_split (pair.second, ';');
316
317       if (states.size() != 2)
318         {
319           log_error ("%s:%s: Invalid state '%s' for '%s'",
320                      SRCNAME, __func__, anonstr (mbox.c_str ()),
321                      anonstr (pair.second.c_str ()));
322           continue;
323         }
324
325       WKSState state = (WKSState) strtol (states[0].c_str (), nullptr, 10);
326       if (state == PublishInProgress)
327         {
328           /* Probably an error during the last publish. Let's start again. */
329           update_state (mbox, NotChecked, false);
330           continue;
331         }
332
333       time_t update_time = (time_t) strtol (states[1].c_str (), nullptr, 10);
334       update_state (mbox, state, false);
335       update_last_checked (mbox, update_time, false);
336     }
337 }
338
339 void
340 WKSHelper::save () const
341 {
342   gpgol_lock (&wks_lock);
343   for (const auto &pair: s_states)
344     {
345       auto state = std::to_string (pair.second) + ';';
346
347       const auto it = s_last_checked.find (pair.first);
348       if (it != s_last_checked.end ())
349         {
350           state += std::to_string (it->second);
351         }
352       else
353         {
354           state += '0';
355         }
356       if (store_extension_subkey_value (WKS_REG_KEY, pair.first.c_str (),
357                                         state.c_str ()))
358         {
359           log_error ("%s:%s: Failed to store state.",
360                      SRCNAME, __func__);
361         }
362     }
363   gpgol_unlock (&wks_lock);
364 }
365
366 static DWORD WINAPI
367 do_notify (LPVOID arg)
368 {
369   /** Wait till a message was sent */
370   std::pair<char *, int> *args = (std::pair<char *, int> *) arg;
371
372   Sleep (args->second);
373   do_in_ui_thread (WKS_NOTIFY, args->first);
374   delete args;
375
376   return 0;
377 }
378
379 void
380 WKSHelper::allow_notify (int sleepTimeMS) const
381 {
382   gpgol_lock (&wks_lock);
383   for (auto &pair: s_states)
384     {
385       if (pair.second == ConfirmationSeen ||
386           pair.second == NeedsPublish)
387         {
388           auto *args = new std::pair<char *, int> (xstrdup (pair.first.c_str()),
389                                                    sleepTimeMS);
390           CloseHandle (CreateThread (nullptr, 0, do_notify,
391                                      args, 0,
392                                      nullptr));
393           break;
394         }
395     }
396   gpgol_unlock (&wks_lock);
397 }
398
399 void
400 WKSHelper::notify (const char *cBox) const
401 {
402   std::string mbox = cBox;
403
404   const auto state = get_state (mbox);
405
406   if (state == NeedsPublish)
407     {
408       char *buf;
409       gpgrt_asprintf (&buf, _("A Pubkey directory is available for the address:\n\n"
410                               "\t%s\n\n"
411                               "Register your Pubkey in that directory to make\n"
412                               "it easy for others to send you encrypted mail.\n\n"
413                               "It's secure and free!\n\n"
414                               "Register automatically?"), mbox.c_str ());
415       memdbg_alloc (buf);
416       if (gpgol_message_box (get_active_hwnd (),
417                              buf,
418                              _("GpgOL: Pubkey directory available!"), MB_YESNO) == IDYES)
419         {
420           start_publish (mbox);
421         }
422       else
423         {
424           update_state (mbox, PublishDenied);
425         }
426       xfree (buf);
427       return;
428     }
429   if (state == ConfirmationSeen)
430     {
431       handle_confirmation_notify (mbox);
432       return;
433     }
434
435   log_debug ("%s:%s: Unhandled notify state: %i for '%s'",
436              SRCNAME, __func__, state, anonstr (cBox));
437   return;
438 }
439
440 void
441 WKSHelper::start_publish (const std::string &mbox) const
442 {
443   log_debug ("%s:%s: Start publish for '%s'",
444              SRCNAME, __func__, mbox.c_str ());
445
446   update_state (mbox, PublishInProgress);
447   const auto key = GpgME::Key::locate (mbox.c_str ());
448
449   if (key.isNull ())
450     {
451       MessageBox (get_active_hwnd (),
452                   "WKS publish failed to find key for mail address.",
453                   _("GpgOL"),
454                   MB_ICONINFORMATION|MB_OK);
455       return;
456     }
457
458   const auto wksPath = get_wks_client_path ();
459
460   if (wksPath.empty())
461     {
462       TRACEPOINT;
463       return;
464     }
465
466   std::vector<std::string> args;
467
468   args.push_back (wksPath);
469   args.push_back (std::string ("--create"));
470   args.push_back (std::string (key.primaryFingerprint ()));
471   args.push_back (mbox);
472
473   // Spawn the process
474   auto ctx = GpgME::Context::createForEngine (GpgME::SpawnEngine);
475   if (!ctx)
476     {
477       TRACEPOINT;
478       return;
479     }
480
481   GpgME::Data mystdin, mystdout, mystderr;
482
483   char **cargs = vector_to_cArray (args);
484
485   GpgME::Error err = ctx->spawn (cargs[0], const_cast <const char **> (cargs),
486                                  mystdin, mystdout, mystderr,
487                                  GpgME::Context::SpawnNone);
488   release_cArray (cargs);
489
490   if (err)
491     {
492       log_debug ("%s:%s: WKS client spawn code: %i asString: %s",
493                  SRCNAME, __func__, err.code(), err.asString());
494       return;
495     }
496   const auto data = mystdout.toString ();
497
498   if (data.empty ())
499     {
500       gpgol_message_box (get_active_hwnd (),
501                          mystderr.toString().c_str (),
502                          _("GpgOL: Directory request failed"),
503                          MB_OK);
504       return;
505     }
506
507   log_data ("%s:%s: WKS client: returned '%s'",
508              SRCNAME, __func__, data.c_str ());
509
510   if (!send_mail (data))
511     {
512       gpgol_message_box (get_active_hwnd (),
513                          _("You might receive a confirmation challenge from\n"
514                            "your provider to finish the registration."),
515                          _("GpgOL: Registration request sent!"), MB_OK);
516     }
517
518   update_state (mbox, RequestSent);
519   return;
520 }
521
522 void
523 WKSHelper::update_state (const std::string &mbox, WKSState state,
524                          bool store) const
525 {
526   gpgol_lock (&wks_lock);
527   auto it = s_states.find(mbox);
528
529   if (it != s_states.end())
530     {
531       it->second = state;
532     }
533   else
534     {
535       s_states.insert (std::make_pair (mbox, state));
536     }
537   gpgol_unlock (&wks_lock);
538
539   if (store)
540     {
541       save ();
542     }
543 }
544
545 void
546 WKSHelper::update_last_checked (const std::string &mbox, time_t time,
547                                 bool store) const
548 {
549   gpgol_lock (&wks_lock);
550   auto it = s_last_checked.find(mbox);
551   if (it != s_last_checked.end())
552     {
553       it->second = time;
554     }
555   else
556     {
557       s_last_checked.insert (std::make_pair (mbox, time));
558     }
559   gpgol_unlock (&wks_lock);
560
561   if (store)
562     {
563       save ();
564     }
565 }
566
567 int
568 WKSHelper::send_mail (const std::string &mimeData) const
569 {
570   std::istringstream ss(mimeData);
571
572   std::string from;
573   std::string to;
574   std::string subject;
575   std::string withoutHeaders;
576
577   std::getline (ss, from);
578   std::getline (ss, to);
579   std::getline (ss, subject);
580
581   if (from.compare (0, 6, "From: ") || to.compare (0, 4, "To: "),
582       subject.compare (0, 9, "Subject: "))
583     {
584       log_error ("%s:%s: Invalid mime data..",
585                  SRCNAME, __func__);
586       return -1;
587     }
588
589   std::getline (ss, withoutHeaders, '\0');
590
591   from.erase (0, 6);
592   to.erase (0, 4);
593   subject.erase (0, 9);
594
595   rtrim (from);
596   rtrim (to);
597   rtrim (subject);
598
599   LPDISPATCH mail = create_mail ();
600
601   if (!mail)
602     {
603       log_error ("%s:%s: Failed to create mail for request.",
604                  SRCNAME, __func__);
605       return -1;
606     }
607
608   /* Now we have a problem. The created LPDISPATCH pointer has
609      a different value then the one with which we saw the ItemLoad
610      event. But we want to get the mail object. So,.. surpise
611      a Hack! :-) */
612   auto last_mail = Mail::getLastMail ();
613
614   if (!Mail::isValidPtr (last_mail))
615     {
616       log_error ("%s:%s: Invalid last mail %p.",
617                  SRCNAME, __func__, last_mail);
618       return -1;
619     }
620   /* Adding to / Subject etc already leads to changes and events so
621      we set up the state before this. */
622   last_mail->setOverrideMIMEData (mimeData);
623   last_mail->setCryptState (Mail::NeedsSecondAfterWrite);
624
625   if (put_oom_string (mail, "Subject", subject.c_str ()))
626     {
627       TRACEPOINT;
628       gpgol_release (mail);
629       return -1;
630     }
631
632   if (put_oom_string (mail, "To", to.c_str ()))
633     {
634       TRACEPOINT;
635       gpgol_release (mail);
636       return -1;
637     }
638
639   LPDISPATCH account = get_account_for_mail (from.c_str ());
640   if (account)
641     {
642       log_debug ("%s:%s: Found account to change for '%s'.",
643                  SRCNAME, __func__, anonstr (from.c_str ()));
644       put_oom_disp (mail, "SendUsingAccount", account);
645     }
646   gpgol_release (account);
647
648   if (invoke_oom_method (mail, "Save", nullptr))
649     {
650       // Should not happen.
651       log_error ("%s:%s: Failed to save mail.",
652                  SRCNAME, __func__);
653       return -1;
654     }
655   if (invoke_oom_method (mail, "Send", nullptr))
656     {
657       log_error ("%s:%s: Failed to send mail.",
658                  SRCNAME, __func__);
659       return -1;
660     }
661   log_debug ("%s:%s: Done send mail.",
662              SRCNAME, __func__);
663   return 0;
664 }
665
666 static void
667 copy_stream_to_data (LPSTREAM stream, GpgME::Data *data)
668 {
669   HRESULT hr;
670   char buf[4096];
671   ULONG bRead;
672   while ((hr = stream->Read (buf, 4096, &bRead)) == S_OK ||
673          hr == S_FALSE)
674     {
675       if (!bRead)
676         {
677           // EOF
678           return;
679         }
680       data->write (buf, (size_t) bRead);
681     }
682 }
683
684 void
685 WKSHelper::handle_confirmation_notify (const std::string &mbox) const
686 {
687   auto pair = get_cached_confirmation (mbox);
688   GpgME::Data *mimeData = pair.first;
689   Mail *mail = pair.second;
690
691   if (!mail && !mimeData)
692     {
693       log_debug ("%s:%s: Confirmation notify without cached data.",
694                  SRCNAME, __func__);
695
696       /* This happens when we have seen a confirmation but have
697        * not confirmed it and the state was saved. So we go back
698        * to the confirmation sent state and wait until we see
699        * the confirmation the next time. */
700       update_state (mbox, ConfirmationSent);
701       return;
702     }
703
704   /* First ask the user if he wants to confirm */
705   if (gpgol_message_box (get_active_hwnd (),
706                          _("Confirm registration?"),
707                          _("GpgOL: Pubkey directory confirmation"), MB_YESNO) != IDYES)
708     {
709       log_debug ("%s:%s: User aborted confirmation.",
710                  SRCNAME, __func__);
711       delete mimeData;
712
713       /* Next time we read the confirmation we ask again. */
714       update_state (mbox, RequestSent);
715       return;
716     }
717
718   /* Do the confirmation */
719   const auto wksPath = get_wks_client_path ();
720
721   if (wksPath.empty())
722     {
723       TRACEPOINT;
724       return;
725     }
726
727   std::vector<std::string> args;
728
729   args.push_back (wksPath);
730   args.push_back (std::string ("--receive"));
731
732   // Spawn the process
733   auto ctx = GpgME::Context::createForEngine (GpgME::SpawnEngine);
734   if (!ctx)
735     {
736       TRACEPOINT;
737       return;
738     }
739   GpgME::Data mystdout, mystderr;
740
741   char **cargs = vector_to_cArray (args);
742
743   GpgME::Error err = ctx->spawn (cargs[0], const_cast <const char **> (cargs),
744                                  *mimeData, mystdout, mystderr,
745                                  GpgME::Context::SpawnNone);
746   release_cArray (cargs);
747
748   if (err)
749     {
750       log_debug ("%s:%s: WKS client spawn code: %i asString: %s",
751                  SRCNAME, __func__, err.code(), err.asString());
752       return;
753     }
754   const auto data = mystdout.toString ();
755
756   if (data.empty ())
757     {
758       gpgol_message_box (get_active_hwnd (),
759                          mystderr.toString().c_str (),
760                          _("GpgOL: Confirmation failed"),
761                          MB_OK);
762       return;
763     }
764
765   log_data ("%s:%s: WKS client: returned '%s'",
766             SRCNAME, __func__, data.c_str ());
767
768   if (!send_mail (data))
769    {
770      gpgol_message_box (get_active_hwnd (),
771                         _("Your Pubkey can soon be retrieved from your domain."),
772                         _("GpgOL: Request confirmed!"), MB_OK);
773    }
774
775   if (mail && Mail::isValidPtr (mail))
776     {
777       invoke_oom_method (mail->item(), "Delete", nullptr);
778     }
779
780   update_state (mbox, ConfirmationSent);
781 }
782
783 void
784 WKSHelper::handle_confirmation_read (Mail *mail, LPSTREAM stream) const
785 {
786   /* We get the handle_confirmation in the Read event. To do sending
787      etc. we have to move out of that event. For this we prepare
788      the data for later usage. */
789
790   if (!mail || !stream)
791     {
792       TRACEPOINT;
793       return;
794     }
795
796   /* Get the recipient of the confirmation mail */
797   const auto recipients = mail->getRecipients_o ();
798
799   /* We assert that we have one recipient as the mail should have been
800      sent by the wks-server. */
801   if (recipients.size() != 1)
802     {
803       log_error ("%s:%s: invalid recipients",
804                  SRCNAME, __func__);
805       gpgol_release (stream);
806       return;
807     }
808
809   std::string mbox = recipients[0];
810
811   /* Prepare stdin for the wks-client process */
812
813   /* First we need to write the headers */
814   LPMESSAGE message = get_oom_base_message (mail->item());
815   if (!message)
816     {
817       log_error ("%s:%s: Failed to obtain message.",
818                  SRCNAME, __func__);
819       gpgol_release (stream);
820       return;
821     }
822
823   const auto headers = mapi_get_header (message);
824   gpgol_release (message);
825
826   GpgME::Data *mystdin = new GpgME::Data();
827
828   mystdin->write (headers.c_str (), headers.size ());
829
830   /* Then the MIME data */
831   copy_stream_to_data (stream, mystdin);
832   gpgol_release (stream);
833
834   /* Then lets make sure its flushy */
835   mystdin->write (nullptr, 0);
836
837   /* And reset it to start */
838   mystdin->seek (0, SEEK_SET);
839
840   gpgol_lock (&wks_lock);
841   s_confirmation_cache.insert (std::make_pair (mbox, std::make_pair (mystdin, mail)));
842   gpgol_unlock (&wks_lock);
843
844   update_state (mbox, ConfirmationSeen);
845
846   /* Send the window message for notify. */
847   allow_notify (5000);
848 }