// ===========================================================================
//! \file
//! \brief GUI based on FLTK 1.3 / 1.4
//!
//! Copyright (c) 2012-2025 by the developers. See the LICENSE file for details.
//!
//! FLTK 1.3 has internal support for Unicode but limited display capabilities
//! on some platforms (no glyph substitution with X11 backend by default).
//!
//! FLTK 1.4 can be compiled with Pango support for the X11 backend. Doing so
//! gives extended Unicode display capabilities including glyph substitution
//! from multiple fonts.


// =============================================================================
// Include headers

// Include this first for conditional compilation
extern "C"
{
#include "config.h"
}

// C++98
#include <cctype>
#include <climits>
#include <cmath>
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sstream>

// FLTK 1.3
#include <FL/Enumerations.H>
// Define FL_ABI_VERSION for old versions too
#if !defined FL_ABI_VERSION && defined FLTK_ABI_VERSION
#  define FL_ABI_VERSION  FLTK_ABI_VERSION
#endif
#include <FL/Fl.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Check_Button.H>
#include <FL/Fl_File_Chooser.H>
#include <FL/Fl_Group.H>
#include <FL/Fl_Input.H>
#include <FL/Fl_Input_Choice.H>
#include <FL/Fl_Menu_.H>
#include <FL/Fl_Menu_Bar.H>
#include <FL/Fl_Menu_Item.H>
#include <FL/Fl_Multi_Browser.H>
#include <FL/Fl_Pixmap.H>
#include <FL/Fl_PostScript.H>
#include <FL/Fl_Progress.H>
#include <FL/Fl_Printer.H>
#include <FL/Fl_Radio_Round_Button.H>
#include <FL/Fl_Return_Button.H>
#include <FL/Fl_Sys_Menu_Bar.H>
#include <FL/Fl_Text_Buffer.H>
#include <FL/Fl_Text_Display.H>
#include <FL/Fl_Text_Editor.H>
#include <FL/Fl_Tabs.H>
#include <FL/Fl_Tile.H>
#include <FL/Fl_Tree_Item.H>
#include <FL/Fl_Tree.H>
#include <FL/Fl_Widget.H>
#if CFG_DB_DISABLE  // Double buffered windows are slow, use only on request
#  include <FL/Fl_Window.H>
#else  // CFG_DB_DISABLE
#  include <FL/Fl_Double_Window.H>
#endif  // CFG_DB_DISABLE
#include <FL/fl_ask.H>

extern "C"
{
// libbasexx
#include <libbasexx-0/basexx_version.h>
// libssiconv
#include <libssiconv-0/iconv.h>
// libjpiconv
#include <libjpiconv-0/iconv.h>
// libuciconv
#include <libuciconv-0/iconv.h>
}

// Local
extern "C"
{
#include "bdate.h"
#include "conf.h"
#include "core.h"
#include "encoding.h"
#include "extutils.h"
#include "filter.h"
#include "log.h"
#include "nls.h"
#include "xdg.h"
}
#include "main.hxx"
#include "tls.hxx"
#include "ui.hxx"


// =============================================================================
//! \defgroup GUI GUI: Graphical User Interface
//!
//! This graphical user interface is based on FLTK 1.3 and is intended to work
//! with a FLTK 1.3.0 shared library. Using features that are added later is not
//! allowed (without ABI guards) to preserve backward compatibility.
//!
//! \note
//! If you hear bugging beeps when information windows appear, this file is the
//! wrong place to fix. These beeps are created by FLTK internally and can be
//! eliminated by patching the file 'fltk-1.3.0/src/fl_ask.cxx'. Commenting out
//! all calls to 'fl_beep()' and rebuilding FLTK will give you comfortable
//! silence. Newer FLTK versions already contains this "silence patch".
//! @{


// =============================================================================
// Experimental features

//! Setting this to 1 enables line count in article tree/list (EXPERIMENTAL)
#define USE_LINE_COUNT  0

//! Setting this to 1 enables article number in article tree/list (EXPERIMENTAL)
#define USE_ARTICLE_NUMBER  0


// =============================================================================
// Constants

//! \brief Message prefix for MAIN module
#define MAIN_ERR_PREFIX  "GUI: "

//! \name Fixed widget colors
//!
//! Override some FLTK defaults that can't be configured via X resources.
//! @{
//! Selection color for menu
#define UI_COLOR_MENU_SELECTION  (Fl_Color) 0x50505000UL
//! Color for progress bar
#define UI_COLOR_PROGRESS_BAR  (Fl_Color) 0x50505000UL
//! Color for selected radio button
#define UI_COLOR_RADIO_BUTTON  (Fl_Color) 0x50505000UL
//! @}

//! Number of styles for article content syntax highlighting
#define UI_STYLES_LEN  9

//! \name Callback action controls
//! @{
#define UI_CB_START     0
#define UI_CB_CONTINUE  1
#define UI_CB_FINISH    2
//! @}

//! \name Callback cookies
//! @{
#define UI_CB_COOKIE_SERVER         0U
#define UI_CB_COOKIE_GROUPLIST      1U
#define UI_CB_COOKIE_GROUPLABELS    2U
#define UI_CB_COOKIE_GROUPPROPOSAL  3U
#define UI_CB_COOKIE_GROUPINFO1     4U
#define UI_CB_COOKIE_GROUPINFO2     5U
#define UI_CB_COOKIE_GROUP          6U
#define UI_CB_COOKIE_OVERVIEW       7U
#define UI_CB_COOKIE_HEADER         8U
#define UI_CB_COOKIE_BODY           9U
#define UI_CB_COOKIE_MOTD           10U
#define UI_CB_COOKIE_ARTICLE        11U
#define UI_CB_COOKIE_SRC            12U
#define UI_CB_COOKIE_POST           13U
//! @}

//! \name Encryption algorithms
//!
//! All nonzero values additionally provide server to client authentication via
//! X509 certificate.
//! @{
#define UI_ENC_NONE    0  // No encryption
#define UI_ENC_STRONG  1  // TLS with strong encryption and forward secrecy
#define UI_ENC_WEAK    2  // TLS compatibility mode offering weak cipher suites
//! @}

//! \name Authentication algorithms (client to server)
//! @{
#define UI_AUTH_NONE  0  // No authentication
#define UI_AUTH_USER  1  // AUTHINFO USER/PASS as defined in RFC 4643
//! @}

//! \name Limits for clamp article count (CONF_CAC) configuration value
//! @{
#define UI_CAC_MIN  10     // Lower limit: 1
#define UI_CAC_MAX  50000  // Upper limit: INT_MAX
//! @}

//! Size of buffer for header field creation
#define UI_HDR_BUFSIZE  (std::size_t) 998

//! Static buffer size for compose window style update callback
#define UI_STATIC_STYLE_BUFSIZE  (std::size_t) 80

//! Maximum number of groups for crossposting
#define UI_XPOST_LIMIT  (std::size_t) 10

//! Enable references line in ArticleWindow class
//!
//! This is currently not useful, because ArticleWindow has no hyperlink
//! support.
#define UI_AW_REFERENCES  0

// Use window class with or without double buffering
#if CFG_DB_DISABLE
#  define UI_WINDOW_CLASS  Fl_Window
#else  // CFG_DB_DISABLE
#  define UI_WINDOW_CLASS  Fl_Double_Window
#endif  // CFG_DB_DISABLE

// Enable window icons on X11 if possible
#define USE_WINDOW_ICON  0
#ifdef FL_ABI_VERSION
#  if 10303 <= FL_ABI_VERSION
      // At least FLTK 1.3.3 is required
#     undef USE_WINDOW_ICON
#     define USE_WINDOW_ICON  1
#  endif  // 10303 <= FL_ABI_VERSION
#endif  // FL_ABI_VERSION


// =============================================================================
// Data types

// Target position for scrolling
enum  ui_scroll
{
   UI_SCROLL_NONE,
   UI_SCROLL_TOP,
   UI_SCROLL_MIDDLE,
   UI_SCROLL_BOTTOM
};

// Server configuration
class ServerConfig
{
   public:
      char server[128];
      char service[6];
      int  enc;  // Use UI_ENC_xxx constants
      int  auth;  // Use UI_AUTH_xxx constants

      inline void  serverReplace(const char*  s)
      {
         std::strncpy(server, s, 128);  server[127] = 0;
      }
      inline void  serviceReplace(const char*  s)
      {
         std::strncpy(service, s, 6);  service[5] = 0;
      }
      inline void  serviceReplace(unsigned int  i)
      {
         std::ostringstream  ss;

         if(0xFFFFU < i)  { i = 0xFFFFU; }
         ss << i << std::flush;
         const std::string&  s = ss.str();
         std::strncpy(service, s.c_str(), 6);  service[5] = 0;
      }
};

// MIME multipart content
class MIMEContent
{
   struct MIMEContentListElement
   {
      enc_mime_cte  cte;
      enc_mime_ct  ct;
      const char*  header;
      const char*  content;
      enum enc_mime_cd  type;  // Content-Disposition
      const char*  filename;  // From Content-Disposition "filename" parameter
      MIMEContentListElement*  next;
   };

   private:
      bool  multipart;  // Multipart flag (to enable display of entity headers)
      std::size_t  partNum;  // Number of elements in linked list
      MIMEContentListElement*  partList;  // Linked list

      const char*  createMessageHeader(const char*, std::size_t);
      MIMEContentListElement*  decodeElement(const char*, std::size_t, char*,
                                             bool, bool, std::size_t*);
      MIMEContentListElement*  initElement(const char*, std::size_t,
                                           enc_mime_cte, enc_mime_ct*,
                                           struct core_article_header*);
   public:
      inline bool  is_multipart(void)  { return(multipart); }
      inline std::size_t  parts(void)  { return(partNum); }
      inline const char*  part_header(std::size_t  i)
      {
         MIMEContentListElement*  mcle = partList;

         if(!partNum || i >= partNum)  { return(NULL); }
         else
         {
            while(i--)  { mcle = mcle->next; }
            return(mcle->header);
         }
      }
      inline const char*  part(std::size_t  i, enc_mime_cte*  te,
                               enc_mime_ct**  ctp)
      {
         MIMEContentListElement*  mcle = partList;

         if(!partNum || i >= partNum)  { return(NULL); }
         else
         {
            while(i--)  { mcle = mcle->next; }
            *te = mcle->cte;
            if(NULL != ctp)  { *ctp = &mcle->ct; }
            return(mcle->content);
         }
      }
      inline enum enc_mime_cd  type(std::size_t  i)
      {
         MIMEContentListElement*  mcle = partList;

         if(!partNum || i >= partNum)  { return(ENC_CD_UNKNOWN); }
         else
         {
            while(i--)  { mcle = mcle->next; }
            return(mcle->type);
         }
      }
      inline const char*  filename(std::size_t  i)
      {
         MIMEContentListElement*  mcle = partList;

         if(!partNum || i >= partNum)  { return(NULL); }
         else
         {
            while(i--)  { mcle = mcle->next; }
            return(mcle->filename);
         }
      }
      MIMEContent(struct core_article_header*, const char*);
      ~MIMEContent(void);
};

// A clone of Fl_Multi_Browser
// Should work like Fl_Hold_Browser, but with focus navigation via keyboard
class My_Multi_Browser : public Fl_Multi_Browser
{
   private:
      int  handle(int);
   public:
      My_Multi_Browser(int  X, int  Y, int  W, int  H, const char*  L = 0)
         : Fl_Multi_Browser(X, Y, W, H, L)  { }
};

// A clone of Fl_Text_Display
class My_Text_Display : public Fl_Text_Display
{
   private:
      int  linkPushed;

      int  handle(int);
   public:
      My_Text_Display(int  X, int  Y, int  W, int  H, const char*  L = 0)
         : Fl_Text_Display(X, Y, W, H, L)  { linkPushed = -1; }
};

// A clone of Fl_Tree
class My_Tree : public Fl_Tree
{
   private:
      bool  updated;  // Used to block article selection while updating
      bool  positions_recalculated;
      Fl_Tree_Item*  current_article;

      int  handle(int);
   public:
      inline void  update_in_progress(bool  state)  { updated = !state; }
      inline bool  update_in_progress(void)  { return !updated; }
      inline void  not_drawn(void)  { positions_recalculated = false; }
      inline bool  drawn(void)
      {
         // Workaround for "infinite busy" without drawing area
         if (0 == w() || 0 == h())  { return true; }
         return(positions_recalculated);
      }
      inline void  draw(void)
      {
         // This will calculate the positions of the tree items
         Fl_Tree::draw();
         positions_recalculated = true;
      }
      inline void  store_current(Fl_Tree_Item*  ti)
      {
         current_article = ti;
      }
      inline void  select_former(void)
      {
         if(NULL != current_article)
         {
            set_item_focus(current_article);
            select_only(current_article, 0);
         }
      }
      My_Tree(int  X, int  Y, int  W, int  H, const char*  L = 0)
         : Fl_Tree(X, Y, W, H, L)
      {
         updated = true;
         positions_recalculated = false;
         current_article = NULL;
      }
};

// Server configuration window
class ServerCfgWindow : public UI_WINDOW_CLASS
{
   private:
      ServerConfig*  sconf;
      int  finished;
      Fl_Group*  grpAuth;
      Fl_Group*  scfgGroup;
      Fl_Input*  scfgHostname;
      Fl_Input*  scfgService;
      Fl_Radio_Round_Button*  scfgTlsOff;
      Fl_Radio_Round_Button*  scfgTlsStrong;
      Fl_Radio_Round_Button*  scfgTlsWeak;
      Fl_Radio_Round_Button*  scfgAuthOff;
      Fl_Radio_Round_Button*  scfgAuthUser;
      char*  hostname;
      char*  service;
      char*  tls_headline;

      // OK callback
      inline void  ok_cb_i(void)  { finished = 1; }
      static void  ok_cb(Fl_Widget*, void*  w)
      {
         ((ServerCfgWindow*) w)->ok_cb_i();
      }

      // Cancel callback
      inline void  cancel_cb_i(void)  { finished = -1; }
      static void  cancel_cb(Fl_Widget*, void*  w)
      {
         ((ServerCfgWindow*) w)->cancel_cb_i();
      }

      // Encryption radio buttons callback
      inline void  enc_cb_i(bool  enc)
      {
         if(!enc)
         {
            scfgService->value("nntp");
#if !CFG_NNTP_AUTH_UNENCRYPTED
            scfgAuthOff->setonly();
            grpAuth->deactivate();
#endif
         }
         else
         {
            scfgService->value("nntps");
            grpAuth->activate();
         }
      }
      static void  enc_off_cb(Fl_Widget*, void*  w)
      {
         ((ServerCfgWindow*) w)->enc_cb_i(false);
      }
      static void  enc_on_cb(Fl_Widget*, void*  w)
      {
         ((ServerCfgWindow*) w)->enc_cb_i(true);
      }

   public:
      int  process(void);
      ServerCfgWindow(ServerConfig*, const char*);
      ~ServerCfgWindow(void);
};

// Identity configuration window
class IdentityCfgWindow : public UI_WINDOW_CLASS
{
   private:
      Fl_Group*  cfgGroup;
      Fl_Input*  fromName;
      Fl_Input*  fromEmail;
      Fl_Input*  replytoName;
      Fl_Input*  replytoEmail;

      // OK callback
      inline void  ok_cb_i(void);
      static void  ok_cb(Fl_Widget*, void*  w)
      {
         ((IdentityCfgWindow*) w)->ok_cb_i();
      }

      // Cancel callback
      static void  cancel_cb(Fl_Widget*, void*  w)
      {
         Fl::delete_widget((Fl_Widget*) w);
      }
   public:
      IdentityCfgWindow(const char*);
      ~IdentityCfgWindow(void);
};

// Misc configuration window
class MiscCfgWindow : public UI_WINDOW_CLASS
{
   private:
      Fl_Tabs*  cfgTabs;
      Fl_Group*  cacGroup;
      Fl_Input*  cacField;
      Fl_Check_Button*  cmprEnable;
      Fl_Check_Button*  localTime;
      Fl_Check_Button*  uagentEnable;
      Fl_Group*  qsGroup;
      Fl_Check_Button*  qsSpace;
      Fl_Check_Button*  qsUnify;

      // OK callback
      inline void  ok_cb_i(void);
      static void  ok_cb(Fl_Widget*, void*  w)
      {
         ((MiscCfgWindow*) w)->ok_cb_i();
      }

      // Cancel callback
      static void  cancel_cb(Fl_Widget*, void*  w)
      {
         Fl::delete_widget((Fl_Widget*) w);
      }
   public:
      MiscCfgWindow(const char*);
      ~MiscCfgWindow(void);
};


// Search window
class SearchWindow : public UI_WINDOW_CLASS
{
   private:
      Fl_Input*  searchField;
      Fl_Check_Button*  cisEnable;
      const char**  currentSearchString;

      // OK callback
      inline void  ok_cb_i(void);
      static void  ok_cb(Fl_Widget*, void*  w)
      {
         ((SearchWindow*) w)->ok_cb_i();
         ((SearchWindow*) w)->finished = 1;
      }

      // Cancel callback
      static void  cancel_cb(Fl_Widget*, void*  w)
      {
         ((SearchWindow*) w)->finished = -1;
      }
   public:
      int  finished;
      SearchWindow(const char*, const char**);
      ~SearchWindow(void);
};


// Protocol console window
class ProtocolConsole : public UI_WINDOW_CLASS
{
   public:
      Fl_Text_Display*  consoleDisplay;
   private:
      Fl_Text_Buffer*  consoleText;
      FILE*  logfp;
      int  nolog;

      // Exit callback
      static void  exit_cb(Fl_Widget*, void*  w)
      {
         Fl::delete_widget((Fl_Widget*) w);
      }

   public:
      void  update(void);
      ProtocolConsole(const char*);
      ~ProtocolConsole(void);
};

// Subscribe window
class SubscribeWindow : public UI_WINDOW_CLASS
{
   private:
      Fl_Group*  subscribeGroup;
      Fl_Tree*  subscribeTree;
      core_groupdesc*  grouplist;
      core_grouplabel*  grouplabels;

      // OK callback
      inline void  ok_cb_i(void);
      static void  ok_cb(Fl_Widget*, void*  w)
      {
         ((SubscribeWindow*) w)->ok_cb_i();
      }

      // Cancel callback
      static void  cancel_cb(Fl_Widget*, void*  w)
      {
         Fl::delete_widget((Fl_Widget*) w);
      }

      // Tree callback
      inline void  tree_cb_i(void)
      {
         Fl_Tree_Item*  ti;

         if(FL_TREE_REASON_SELECTED == subscribeTree->callback_reason())
         {
            // Immediately reset selection again if item is not selectable
            ti = subscribeTree->callback_item();
            if(NULL == ti->user_data())  { subscribeTree->deselect(ti, 0); }
         }
      }
      static void  tree_cb(Fl_Widget*, void*  w)
      {
         ((SubscribeWindow*) w)->tree_cb_i();
      }
   public:
      inline void  add(const char*  entry, core_anum_t  num, const char*  label)
      {
         Fl_Tree_Item*  ti;
         std::ostringstream  labelString;

         ti = subscribeTree->find_item(entry);
         if(NULL == ti)  { ti = subscribeTree->add(entry); }
         if(NULL == ti)
         {
            PRINT_ERROR("Adding group to subscription tree failed");
         }
         else
         {
            // Set all selectable items to bold font and tag them with user data
            ti->labelfont(FL_HELVETICA_BOLD);
            ti->user_data((void*) entry);
            labelString << ti->label() << " (" << num << ")";
            if(NULL != label)  { labelString << "   |   " << label; }
            // Attention:
            //
            //    const char*  ls = labelString.str().c_str()
            //
            // creates 'ls' with undefined value because the compiler is
            // allowed to free the temporary string object returned by
            // 'str()' immediately after the assignment!
            // Assigning names to temporary string objects forces them to
            // stay in memory as long as their names go out of scope (this
            // is what we need).
            const std::string&  ls = labelString.str();
            ti->label(ls.c_str());
         }
      }
      inline void  collapseAll(void)
      {
         Fl_Tree_Item*  i;

         for(i = subscribeTree->first(); i; i = subscribeTree->next(i))
         {
            subscribeTree->close(i, 0);
         }
         subscribeTree->open(subscribeTree->root(), 0);
      }
      SubscribeWindow(const char*  label, core_groupdesc*  glist,
                      core_grouplabel*  labels);
      ~SubscribeWindow(void);
};

// MID search window
class MIDSearchWindow : public UI_WINDOW_CLASS
{
   private:
      Fl_Group*  cfgGroup;
      Fl_Input*  mid;

      // OK callback
      inline void  ok_cb_i(void);
      static void  ok_cb(Fl_Widget*, void*  w)
      {
         ((MIDSearchWindow*) w)->ok_cb_i();
      }

      // Cancel callback
      static void  cancel_cb(Fl_Widget*, void*  w)
      {
         Fl::delete_widget((Fl_Widget*) w);
      }
   public:
      MIDSearchWindow(const char*);
      ~MIDSearchWindow(void);
};

// Bug report window
class BugreportWindow : public UI_WINDOW_CLASS
{
   private:
      Fl_Text_Display*  bugreportDisplay;
      Fl_Text_Buffer*  bugreportText;

      // Exit callback
      static void  exit_cb(Fl_Widget*, void*  w)
      {
         Fl::delete_widget((Fl_Widget*) w);
      }

   public:
      BugreportWindow(const char*, const char*);
      ~BugreportWindow(void);
};

// License window
class LicenseWindow : public UI_WINDOW_CLASS
{
   private:
      Fl_Text_Display*  licenseDisplay;
      Fl_Text_Buffer*  licenseText;

      // Exit callback
      static void  exit_cb(Fl_Widget*, void*  w)
      {
         Fl::delete_widget((Fl_Widget*) w);
      }

   public:
      LicenseWindow(const char*);
      ~LicenseWindow(void);
};

// Message of the day window
class MotdWindow : public UI_WINDOW_CLASS
{
   private:
      Fl_Text_Display*  motdDisplay;
      Fl_Text_Buffer*  motdText;

      // Exit callback
      static void  exit_cb(Fl_Widget*, void*  w)
      {
         Fl::delete_widget((Fl_Widget*) w);
      }

   public:
      MotdWindow(const char*, const char*);
      ~MotdWindow(void);
};

// Article window
class ArticleWindow : public UI_WINDOW_CLASS
{
   private:
      Fl_Group*  articleGroup;
      My_Text_Display*  articleDisplay;
      Fl_Text_Buffer*  articleText;
      Fl_Text_Buffer*  articleStyle;
      Fl_Text_Display::Style_Table_Entry*  styles;
      int  styles_len;
      MIMEContent*  mimeData;
      struct core_hierarchy_element*  alt_hier;

      const char*  printHeaderFields(struct core_article_header*);
      void  articleUpdate(Fl_Text_Buffer*  article);

      // Exit callback
      static void  cancel_cb(Fl_Widget*, void*  w)
      {
         Fl::delete_widget((Fl_Widget*) w);
      }

   public:
      ArticleWindow(const char*, const char*);
      ~ArticleWindow(void);
};

// Article source window
class ArticleSrcWindow : public UI_WINDOW_CLASS
{
   private:
      Fl_Group*  srcGroup;
      Fl_Text_Display*  srcDisplay;
      Fl_Text_Buffer*  srcText;
      Fl_Text_Buffer*  srcStyle;
      Fl_Text_Display::Style_Table_Entry*  styles;
      char*  srcArticle;
      const char*  pathname;

      // Save callback
      inline void  save_cb_i(void);
      static void  save_cb(Fl_Widget*, void*  w)
      {
         ((ArticleSrcWindow*) w)->save_cb_i();
      }

      // Exit callback
      static void  cancel_cb(Fl_Widget*, void*  w)
      {
         Fl::delete_widget((Fl_Widget*) w);
      }

   public:
      ArticleSrcWindow(const char*, const char*);
      ~ArticleSrcWindow(void);
};

// Compose window
class ComposeWindow : public UI_WINDOW_CLASS
{
   private:
      Fl_Tabs*  compTabs;
      Fl_Group*  subjectGroup;
      Fl_Input*  subjectField;
      Fl_Group*  compGroup;
      Fl_Group*  uriEncGroup;
      Fl_Group*  advancedGroup;
      Fl_Text_Buffer*  compHeader;
      Fl_Text_Display::Style_Table_Entry*  styles;
      Fl_Text_Buffer*  currentStyle;
      Fl_Text_Buffer*  compText;
      // On URI decoder tab
      Fl_Box*  uriHeaderField;
      Fl_Input_Choice*  uriSchemeField;
      Fl_Input*  uriBodyField;
      // On advanced tab
      Fl_Input*  newsgroupsField;
      Fl_Input_Choice*  fup2Field;
      Fl_Input_Choice*  keywordField;
      Fl_Input*  expireField;
      Fl_Input_Choice*  distriField;
      Fl_Check_Button*  archiveButton;
      Fl_Box*  fillSpace;

      // Change subject callback
      inline void  change_cb_i(void);
      static void  change_cb(Fl_Widget*, void*  w)
      {
         ((ComposeWindow*) w)->change_cb_i();
      }

      // Style update callback
      inline void  style_update_cb_i(int  pos, int  nInserted, int  nDeleted,
                                     Fl_Text_Buffer*  style,
                                     Fl_Text_Editor*  editor);
      static void  style_update_cb(int  pos, int  nInserted, int  nDeleted,
                                   int  nRestyled, const char*  deletedText,
                                   void*  w)
      {
         (void) nRestyled;
         (void) deletedText;
         ((ComposeWindow*) w)->style_update_cb_i(pos, nInserted, nDeleted,
                                             ((ComposeWindow*) w)->currentStyle,
                                             ((ComposeWindow*) w)->compEditor);
      }

      // Send callback
      inline void  send_cb_i(void);
      static void  send_cb(Fl_Widget*, void*  w)
      {
         ((ComposeWindow*) w)->send_cb_i();
      }

      // Cancel callback
      inline void  cancel_cb_i(Fl_Widget*);
      static void  cancel_cb(Fl_Widget*, void*  w)
      {
         ((ComposeWindow*) w)->cancel_cb_i((Fl_Widget*) w);
      }

      // URI insert callback
      inline void  uri_insert_cb_i(void);
      static void  uri_insert_cb(Fl_Widget*, void*  w)
      {
         ((ComposeWindow*) w)->uri_insert_cb_i();
      }

      int  searchHeaderField(const char*, int*, int*);
      const char*  extractHeaderField(const char*);
      int  replaceHeaderField(const char*, const char*);
      void  deleteHeaderField(const char*);
      int  checkArticleBody(const char*);
   public:
      Fl_Text_Editor*  compEditor;
      ComposeWindow(const char*, const char*, const char*, const char*,
                    struct core_article_header*, bool);
      ~ComposeWindow(void);
};

// Main window
class  MainWindow : public UI_WINDOW_CLASS
{
   // States for the callback locking state machine
   enum mainWindowState
   {
      STATE_READY,
      STATE_MUTEX,  // Allow nothing else to run in parallel
      STATE_SERVER1,
      STATE_SERVER2,
      STATE_PROPOSAL,
      STATE_GROUP,
      STATE_SCROLL,
      STATE_NEXT,
      STATE_COMPOSE,
      STATE_POST
   };

   // Events for the callback locking state machine
   enum mainWindowEvent
   {
      // Server configuration
      EVENT_SERVER,
      EVENT_SERVER_EXIT,
      // Group subscription
      EVENT_SUBSCRIBE,
      EVENT_SUBSCRIBE_EXIT,
      // Group list proposals
      EVENT_GL_PROPOSAL,
      EVENT_GL_PROPOSAL_EXIT,
      // Group list refresh
      EVENT_GL_REFRESH,
      EVENT_GL_REFRESH_EXIT,
      // Group selection
      EVENT_G_SELECT,
      EVENT_G_SELECT_EXIT,
      // Article tree refresh
      EVENT_AT_REFRESH,
      EVENT_AT_REFRESH_EXIT,
      // Prepare for article selection
      EVENT_A_PREPARE,
      // Article selection
      EVENT_A_SELECT,
      EVENT_A_SELECT_EXIT,
      // View article (not from current group)
      EVENT_A_VIEW,
      EVENT_A_VIEW_EXIT,
      // Mark articles read (current group)
      EVENT_A_MAAR,
      EVENT_A_MAAR_EXIT,
      // Mark articles read (all groups)
      EVENT_A_MAGAR,
      EVENT_A_MAGAR_EXIT,
      // View source code
      EVENT_SRC_VIEW,
      EVENT_SRC_VIEW_EXIT,
      // View message of the day
      EVENT_MOTD_VIEW,
      EVENT_MOTD_VIEW_EXIT,
      // Scroll down or (at the end) select next unread article
      EVENT_SCROLL_NEXT,
      EVENT_SCROLL_NEXT_EXIT,
      // Article composition
      EVENT_COMPOSE,
      EVENT_COMPOSE_EXIT,
      // Article posting
      EVENT_POST,
      EVENT_POST_EXIT
   };

   public:
      Fl_Box*  statusBar;
      Fl_Progress*  progressBar;
      std::ostringstream  aboutString;
      ComposeWindow*  composeWindow;
      int  composeWindowLock;
      unsigned int  hyperlinkStyle;
      int  hyperlinkPosition;
   private:
      bool  startup;  // Flag to preserve greeting message on startup
      bool  busy;  // Mouse cursor state
      bool  unsub;  // Flag to indicate current group was unsubscribed
      mainWindowState  mainState;
      SubscribeWindow*  subscribeWindow;
      Fl_Tile*  contentGroup;
      Fl_Tile*  contentGroup2;
#if CFG_COCOA_SYS_MENUBAR
      Fl_Sys_Menu_Bar*  menu;
#else  // CFG_COCOA_SYS_MENUBAR
      Fl_Menu_Bar*  menu;
#endif  // CFG_COCOA_SYS_MENUBAR
      std::size_t  groupcount;
      core_groupdesc*  grouplist;
      My_Multi_Browser*  groupList;
      core_groupdesc*  subscribedGroups;  // Array of subscribed groups
      core_groupdesc*  currentGroup;  // Descriptor of current group
      My_Tree*  articleTree;
      core_hierarchy_element*  currentArticleHE;
      core_hierarchy_element*  lastArticleHE;
      My_Text_Display*  text;
      int  wrapMode;
      Fl_Text_Display::Style_Table_Entry*  styles;
      int  styles_len;
      Fl_Text_Buffer*  currentStyle;
      Fl_Text_Buffer*  currentArticle;
      int  currentLine;
      const char*  currentSearchString;
      int  currentSearchPosition;
      int  state;  // -1: Abort, 0: Ready, 1: Finished, 2: Empty
      core_anum_t  ai;  // Article ID (watermark)
      core_range  ai_range;  // Article ID (watermark) range
      int  groupSelect_cb_state;
      int  groupRefresh_cb_state;
      float  progress_percent_value;
      char  progress_percent_label[5];
      bool  progress_skip_update;
      MIMEContent*  mimeData;
      char*  mid_a;
      bool  rot13;  // ROT13 applied. Recovery requires reload of article

      // The group states are loaded/stored from/to the groupfile
      // Note:
      // The 'name' fields in the group descriptor array 'subscribedGroups'
      // point to the memory of the corresponding 'name' fields of 'group_list'.
      core_groupstate*  group_list;  // Array of group states
      std::size_t  group_num;  // Number of elements in group state array
      std::size_t  group_list_index;  // Index of current group in 'group_list'
      int  group_old;  // Previous selected value in 'groupList' widget
      int  group_new;  // Selected value in 'groupList' widget

      // Exit callback
      inline void  progress_release_cb_i(void)
      {
         progress_skip_update = false;
      }
      static void  progress_release_cb(void*  w)
      {
         ((MainWindow*) w)->progress_release_cb_i();
      }

      // Exit callback
      inline void  exit_cb_i(void);
      static void  exit_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->exit_cb_i();
      }

      // Print cooked article callback
      inline void  print_cb_i(void);
      static void  print_cb(Fl_Widget*, void*  w)
      {
#if defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
         // Disable GTK print dialog that offer features that do not work
         Fl::option(Fl::OPTION_PRINTER_USES_GTK, false);
#endif  // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
         ((MainWindow*) w)->print_cb_i();
      }

      // Save cooked article (to file) callback
      inline void  asave_cb_i(void);
      static void  asave_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->asave_cb_i();
      }

      // Server callback
      inline void  server_cb_i(void)
      {
         updateServer(UI_CB_START);
      }
      static void  server_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->server_cb_i();
      }

      // Configuration callback
      inline void  config_cb_i(void);
      static void  config_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->config_cb_i();
      }

      // Identity callback
      inline void  identity_cb_i(void);
      static void  identity_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->identity_cb_i();
      }

      // About information callback
      inline void  about_cb_i(void);
      static void  about_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->about_cb_i();
      }

      // View message of the day callback
      inline void  viewmotd_cb_i(void)
      {
         viewMotd(UI_CB_START);
      }
      static void  viewmotd_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->viewmotd_cb_i();
      }

      // MID search callback
      inline void  mid_search_cb_i(void);
      static void  mid_search_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->mid_search_cb_i();
      }

      // Bug callback
      inline void  bug_cb_i(void);
      static void  bug_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->bug_cb_i();
      }

      // License information callback
      inline void  license_cb_i(void);
      static void  license_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->license_cb_i();
      }

      // Protocol console callback
      inline void  console_cb_i(void);
      static void  console_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->console_cb_i();
      }

      // Subscribe callback
      inline void  gsubscribe_cb_i(void)
      {
         groupSubscribe(UI_CB_START);
      }
      static void  gsubscribe_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->gsubscribe_cb_i();
      }

      // Unsubscribe callback
      inline void  gunsubscribe_cb_i(void)
      {
         std::size_t  index = (std::size_t) groupList->value();
         int  rv;

         // Ask for confirmation
         fl_message_title(S("Warning"));
         rv = fl_choice("%s", S("No"),
                        S("Yes"), NULL,
                        S("Really unsubscribe group?"));
         if(!rv)  { return; }

         if(!index)
         {
            SC("Do not use non-ASCII for the translation of this item")
            fl_message_title(S("Error"));
            fl_alert("%s", S("No group selected"));
         }
         else
         {
            rv = core_unsubscribe_group(&group_num, &group_list,
                                        &group_list_index);
            if(rv)
            {
               fl_message_title(S("Error"));
               fl_alert("%s", S("Unsubscribe operation failed"));
            }
            rv = core_export_group_states(group_num, group_list);
            if(rv)
            {
               fl_message_title(S("Error"));
               fl_alert("%s", S("Exporting group states failed"));
            }
            unsub = true;
            groupListRefresh(UI_CB_START);
         }
      }
      static void  gunsubscribe_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->gunsubscribe_cb_i();
      }

      // Group list refresh callback
      inline void  grefresh_cb_i(void)
      {
         groupListRefresh(UI_CB_START);
      }
      static void  grefresh_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->grefresh_cb_i();
      }

      // Sort group list callback
      inline void  gsort_cb_i(void)
      {
         int  rv;

         // Ask for confirmation
         fl_message_title(S("Warning"));
         rv = fl_choice("%s", S("No"),
                        S("Yes"), NULL,
                        S("Really sort group list?"));
         if(!rv)  { return; }

         rv = core_sort_group_list();
         if(!rv)
         {
            // Set unsubscribe flag to clear tree and current group/article
            unsub = true;
            groupListRefresh(UI_CB_START);
         }
      }
      static void  gsort_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->gsort_cb_i();
      }

      // Group select callback
      inline void  gselect_cb_i(void)
      {
         groupSelect(UI_CB_START, groupList->value());
      }
      static void  gselect_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->gselect_cb_i();
      }

      // Next unread group callback
      inline void  nug_cb_i(void)
      {
         int  i;
         int  groups = groupList->size();
         int  current = groupList->value();  // Zero if no group is selected
         core_anum_t  ur;

      wraparound:
         for(current ? i = current - 1 : i = 0; i < groups; ++i)
         {
            ur = groupGetUnreadNo(subscribedGroups[i].lwm,
                                  subscribedGroups[i].hwm, group_list[i].info);
            if(ur)
            {
               groupList->deselect();
               groupList->select(++i);
               groupSelect(UI_CB_START, i);
               break;
            }
         }
         if (1 < current)
         {
             groups = current - 1;
             current = 0;
             goto wraparound;
         }
      }
      static void  nug_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->nug_cb_i();
      }

      // Threaded view toggle callback
      // This callback should deactivate all menu items not usable in this mode
      inline void  thrv_cb_i(void)
      {
         Fl_Menu_Item*  mi = (Fl_Menu_Item*) menu->find_item(uthrv_sort_cb);

         if(config[CONF_TVIEW].val.i)
         {
            config[CONF_TVIEW].val.i = 0;
            if(NULL != mi)  { mi->activate(); }
         }
         else
         {
            config[CONF_TVIEW].val.i = 1;
            if(NULL != mi)  { mi->deactivate(); }
         }
         if(NULL != currentGroup)  { updateTree(); }
#if CFG_COCOA_SYS_MENUBAR
         menu->update();
#endif  // CFG_COCOA_SYS_MENUBAR
      }
      static void  thrv_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->thrv_cb_i();
      }

      // Unthreaded view sorting based on date or watermark toggle callback
      // Ignored for threaded view
      inline void  uthrv_sort_cb_i(void)
      {
         if(config[CONF_UTVIEW_AN].val.i)  { config[CONF_UTVIEW_AN].val.i = 0; }
         else  { config[CONF_UTVIEW_AN].val.i = 1; }
         if(NULL != currentGroup)  { updateTree(); }
      }
      static void  uthrv_sort_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->uthrv_sort_cb_i();
      }

      // Only unread flag toggle callback
      inline void  onlyur_cb_i(void)
      {
         if(config[CONF_ONLYUR].val.i)  { config[CONF_ONLYUR].val.i = 0; }
         else  { config[CONF_ONLYUR].val.i = 1; }
         if(NULL != currentGroup)  { updateTree(); }
      }
      static void  onlyur_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->onlyur_cb_i();
      }

      // Wrap text to fit width
      inline void  wrap_cb_i(void)
      {
         if(Fl_Text_Display::WRAP_AT_BOUNDS != wrapMode)
         {
            wrapMode = Fl_Text_Display::WRAP_AT_BOUNDS;
         }
         else  { wrapMode = Fl_Text_Display::WRAP_NONE; }
         text->wrap_mode(wrapMode, 0);
      }
      static void  wrap_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->wrap_cb_i();
      }

      // ROT13 encode/decode callback
      inline void  rot13_cb_i(void);
      static void  rot13_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->rot13_cb_i();
      }

      // Mark single article unread callback
      // Now used to toggle (mark read if called again)
      inline void  msau_cb_i(void);
      static void  msau_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->msau_cb_i();
      }

      // Mark subthread read callback
      inline void  mssar(Fl_Tree_Item*);
      inline void  mssar_cb_i(void);
      static void  mssar_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->mssar_cb_i();
      }

      // Mark all read callback
      inline void  maar_cb_i(void);
      static void  maar_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->maar_cb_i();
      }

      // Mark (all articles in) all groups read callback
      inline void  magar_cb_i(void);
      static void  magar_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->magar_cb_i();
      }

      // Article select callback
      inline void  aselect_cb_i(void);
      static void  aselect_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->aselect_cb_i();
      }

      // View source code callback
      inline void  viewsrc_cb_i(void)
      {
         viewSrc(UI_CB_START);
      }
      static void  viewsrc_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->viewsrc_cb_i();
      }

      // Compose callback
      inline void  compose_cb_i(void)
      {
         articleCompose(false, false);
      }
      static void  compose_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->compose_cb_i();
      }

      // Reply callback
      inline void  reply_cb_i(void)
      {
         articleCompose(true, false);
      }
      static void  reply_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->reply_cb_i();
      }

      // Cancel callback
      inline void  cancel_cb_i(void)
      {
         articleCompose(false, true);
      }
      static void  cancel_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->cancel_cb_i();
      }

      // Supersede callback
      inline void  supersede_cb_i(void)
      {
         articleCompose(true, true);
      }
      static void  supersede_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->supersede_cb_i();
      }

      // Reply by e-mail callback
      inline void  email_cb_i(void)
      {
         sendEmail();
      }
      static void  email_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->email_cb_i();
      }

      // Next unread article callback
      inline void  nua_cb_i(void)
      {
         std::size_t  index = (std::size_t) groupList->value();

         if(!index)
         {
            SC("Do not use non-ASCII for the translation of this item")
            fl_message_title(S("Error"));
            fl_alert("%s", S("No group selected"));
         }
         else  { ascrolldown_cb(false); }
      }
      static void  nua_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->nua_cb_i();
      }

      // Previous read article callback
      inline void  pra_cb_i(void)
      {
         const char*  mid;
         std::size_t  len;

         if(NULL == lastArticleHE)
         {
            SC("Do not use non-ASCII for the translation of this item")
            fl_message_title(S("Error"));
            fl_alert("%s", S("No previously read article stored for group"));
         }
         else
         {
            mid = lastArticleHE->header->msgid;
            len = std::strlen(mid);
            // Use Message-ID without angle brackets
            if(NULL == searchSelectArticle(&mid[1], len - (std::size_t) 2))
            {
               SC("Do not use non-ASCII for the translation of this item")
               fl_message_title(S("Error"));
               fl_alert("%s", "Previous article not found (bug)");
            }
         }
      }
      static void  pra_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->pra_cb_i();
      }

      // Hyperlink clicked callback
      inline void  hyperlink_cb_i(void)
      {
         hyperlinkHandler(hyperlinkPosition);
      }
      static void  hyperlink_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->hyperlink_cb_i();
      }

      void  clearTree(void);
      void  scrollTree(ui_scroll, Fl_Tree_Item*);
      bool  checkTreeBranchForUnread(Fl_Tree_Item*);
      bool  checkTreeBranchForItem(Fl_Tree_Item*, Fl_Tree_Item*);
      Fl_Tree_Item*  addTreeNodes(Fl_Tree_Item*, core_hierarchy_element*);
      void  updateTree(void);
      inline void  collapseTree(void)
      {
         int  i;

         for(i = 0; i < articleTree->root()->children(); ++i)
         {
            articleTree->root()->child(i)->close();
         }
      }
      void  groupSubscribe(int);
      core_anum_t  groupGetUnreadNo(core_anum_t, core_anum_t,
                                    struct core_range*);
      void  groupListUpdateEntry(std::size_t);
      int  groupStateMerge(void);
      void  groupSelect(int, int);
      void  groupCAC(core_groupdesc*);
      void  groupListGetProposal(int);
      void  updateArticleTree(int);
      void  articleSelect(int);
      void  viewMotd(int);
      void  viewSrc(int);
      void  stripAngleAddress(char*);
      void  articleCompose(bool, bool);
      void  sendEmail(void);
      Fl_Tree_Item*  searchSelectArticle(const char*, std::size_t);
      void  storeMIMEEntityToFile(const char*, const char*);
      void  hyperlinkHandler(int);
      const char*  printHeaderFields(struct core_article_header*);
      bool  stateMachine(enum mainWindowEvent);
   public:
      void  viewArticle(int, const char*);
      static void  group_list_refresh_timer_cb(void*  w)
      {
         ((MainWindow*) w)->groupListRefresh(UI_CB_START);
         if(0 < config[CONF_REFRESH_INTERVAL].val.i)
         {
            double  iv = 60.0 * (double) config[CONF_REFRESH_INTERVAL].val.i;

            // Start timeout for configured refresh interval
            Fl::add_timeout(iv, group_list_refresh_timer_cb, (void*) w);
         }
      }
      static void  serverconf_cb(void*  w)
      {
         ((MainWindow*) w)->updateServer(UI_CB_CONTINUE);
      }
      static void  subscribe_cb1(void*  w)
      {
         ((MainWindow*) w)->groupSubscribe(UI_CB_CONTINUE);
      }
      static void  subscribe_cb2(void*  w)
      {
         ((MainWindow*) w)->groupSubscribe(UI_CB_FINISH);
      }
      static void  groupproposal_cb(void*  w)
      {
         ((MainWindow*) w)->groupListGetProposal(UI_CB_CONTINUE);
      }
      static void  refresh_cb1(void*  w)
      {
         ((MainWindow*) w)->groupListRefresh(UI_CB_CONTINUE);
      }
      static void  refresh_cb2(void*  w)
      {
         ((MainWindow*) w)->groupListRefresh(UI_CB_FINISH);
      }
      static void  group_cb(void*  w)
      {
         ((MainWindow*) w)->groupSelect(UI_CB_CONTINUE, 0);
      }
      static void  overview_cb(void*  w)
      {
         ((MainWindow*) w)->updateArticleTree(UI_CB_FINISH);
      }
      static void  header_cb(void*  w)
      {
         ((MainWindow*) w)->updateArticleTree(UI_CB_CONTINUE);
      }
      static void  body_cb(void*  w)
      {
         ((MainWindow*) w)->articleSelect(UI_CB_CONTINUE);
      }
      static void  motd_cb(void*  w)
      {
         ((MainWindow*) w)->viewMotd(UI_CB_CONTINUE);
      }
      static void  article_cb(void*  w)
      {
         ((MainWindow*) w)->viewArticle(UI_CB_CONTINUE, NULL);
      }
      static void  src_cb(void*  w)
      {
         ((MainWindow*) w)->viewSrc(UI_CB_CONTINUE);
      }
      static void  post_cb(void*  w)
      {
         ((MainWindow*) w)->articlePost(UI_CB_CONTINUE, NULL);
      }
      // Search in article content
      inline void  asearch_cb_i(void);
      static void  asearch_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->asearch_cb_i();
      }
      void  ascrolldown_cb(bool);
      void  articlePost(int, const char*);
      inline void  composeComplete(void)
      {
         if(!stateMachine(EVENT_COMPOSE_EXIT))
         {
            PRINT_ERROR("Error in main window state machine");
         }
      }
      void  calculatePercent(std::size_t, std::size_t);
      inline int  groupStateExport(void)
      {
         return(core_export_group_states(group_num, group_list));
      }
      void  updateServer(int);
      void  groupListRefresh(int);
      inline void  groupListImport(void)
      {
         core_destroy_subscribed_group_states(&group_num, &group_list);
         groupListRefresh(UI_CB_START);
      }
      void  articleUpdate(Fl_Text_Buffer*);
      inline int  getTilingX(void)  { return(groupList->w()); }
      inline int  getTilingY(void)  { return(articleTree->h()); }
      inline void  setTilingX(int  tx)
      {
         // Enforce some minimum width
         if(50 > tx)  { tx = 50; }
         if(w() - 50 < tx)  { tx = w() - 50; }
         contentGroup
#if defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
         // Requires FLTK 1.4.0
         ->move_intersection
#else  // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
         ->position
#endif  // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
                            (230, 0, tx, 0);
      }
      inline void  setTilingY(int  ty)
      {
         // Enforce some minimum height
         if(50 > ty)  { ty = 50; }
         if(groupList->h() - 50 < ty)  { ty = groupList->h() - 50; }
         contentGroup2
#if defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
         // Requires FLTK 1.4.0
         ->move_intersection
#else  // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
         ->position
#endif  // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
                            (1, contentGroup2->y() + 140,
                             1, contentGroup2->y() + ty);
      }
      MainWindow(const char*);
      ~MainWindow(void);
};


// =============================================================================
// Macros

//! Replace message in main window status bar
#define UI_STATUS(s)  { if(mainWindow)  { mainWindow->statusBar->label(s); } }

//! Clear main window progress bar
#define UI_READY() \
{ \
   if(mainWindow) \
   { \
      mainWindow->progressBar->value(0.0); \
      mainWindow->progressBar->label(""); \
      mainWindow->default_cursor(FL_CURSOR_DEFAULT); \
      mainWindow->busy = false; \
   } \
}

//! Display "Busy" in main window progress bar
#define UI_BUSY() \
{ \
   if(mainWindow) \
   { \
      mainWindow->progressBar->value(0.0); \
      SC("This string must fit into the progress bar box") \
      mainWindow->progressBar->label(S("Busy")); \
      mainWindow->default_cursor(FL_CURSOR_WAIT); \
      mainWindow->busy = true; \
   } \
}

//! Update value of main window progress bar
#define UI_PROGRESS(s, e) \
{ \
   if(mainWindow) \
   { \
      mainWindow->calculatePercent(s, e); \
      if(100.0 == progress_percent_value || !progress_skip_update) \
      { \
         mainWindow->progressBar->value(progress_percent_value); \
         mainWindow->progressBar->copy_label(progress_percent_label); \
      } \
      if(!mainWindow->busy) \
      { \
         mainWindow->default_cursor(FL_CURSOR_WAIT); \
         mainWindow->busy = true; \
      } \
      Fl::check(); \
      progress_skip_update = true; \
      Fl::add_timeout(0.1, progress_release_cb, this); \
   } \
}


// =============================================================================
// Variables

// Pixmaps
extern "C"
{
#include "pixmaps/pixmaps.c"
}
#if USE_WINDOW_ICON
static Fl_Pixmap  pm_window_icon(xpm_window_icon);    // Icon for main window
#endif  // USE_WINDOW_ICON
static Fl_Pixmap  pm_own(xpm_own);                    // Icon for own articles
static Fl_Pixmap  pm_reply_to_own(xpm_reply_to_own);  // Icon for own articles
static Fl_Pixmap  pm_score_down(xpm_score_down);      // Icon for negative score
static Fl_Pixmap  pm_score_up(xpm_score_up);          // Icon for positive score

static Fl_Text_Buffer*  dummyTb;
#if USE_WINDOW_ICON
static Fl_RGB_Image  mainIcon(&pm_window_icon);
#endif  // USE_WINDOW_ICON
static MainWindow*  mainWindow = NULL;
static ProtocolConsole*  protocolConsole = NULL;
static int  exitRequest = 0;
static bool  lockingInitialized = false;
static int  offset_correction_x;
static int  offset_correction_y;


// =============================================================================
// Set default font
// This is for internal size calculations, changing this will not display the
// widgets with a different font!

static void  gui_set_default_font(void)
{
   fl_font(FL_HELVETICA, FL_NORMAL_SIZE);
}


// =============================================================================
// Greeting message (displayed as the default article on startup)

static const char*  gui_greeting(void)
{
   std::ostringstream  greetingString;
   const char*  s1;
   char*  s2;
   std::size_t  len;

   // Create greeting string
   if(!std::strcmp(CFG_NAME, "flnews"))
   {
      greetingString
         << "       ______  ___\n"
         << "      /  ___/ /  /\n"
         << "     /  /    /  /\n"
         << "  __/  /_   /  / _______    _______    ___     ___  _______\n"
         << " /_   __/  /  / /  ___  \\  /  ___  \\  /  /    /  / /  _____\\\n"
         << "  /  /    /  / /  /  /  / /  /__/__/ /  /_/|_/  / /__/_____\n"
         << " /  /    /  / /  /  /  / /  /_____  /   ___    / ______/  /\n"
         << "/__/    /__/ /__/  /__/  \\_______/  \\__/  /___/  \\_______/";
   }
   else  { greetingString << CFG_NAME; }
   greetingString << "\n\n"
      << S("A fast and lightweight Usenet newsreader for Unix.") << "\n\n"
      << CFG_NAME << " "
      // Display credits to the authors of libraries we use
      << S("is based in part on the work of the following projects:") << "\n"
      << "- FLTK project (https://www.fltk.org/)" << "\n"
      << "- The Unicode\xC2\xAE Standard (https://home.unicode.org/)" << "\n"
      << "  Unicode is a registered trademark of Unicode, Inc. in the" << "\n"
      << "  United States and other countries" << "\n"
#if CFG_USE_TLS
#  if CFG_USE_LIBRESSL
      << "- LibreSSL project (https://www.libressl.org/)" << "\n"
#  else  // CFG_USE_LIBRESSL
      << "- OpenSSL project (https://www.openssl.org/)" << "\n"
#  endif  // CFG_USE_LIBRESSL
      << "  This product includes software developed by" << "\n"
      << "  the OpenSSL Project for use in the OpenSSL Toolkit" << "\n"
      << "  This product includes software written by" << "\n"
      << "  Tim Hudson <tjh@cryptsoft.com>" << "\n"
      << "  This product includes cryptographic software written by" << "\n"
      << "  Eric Young <eay@cryptsoft.com>" << "\n"
#endif  // CFG_USE_TLS
#if CFG_USE_ZLIB
      << "- zlib compression library (https://zlib.net/)" << "\n"
#endif  // CFG_USE_ZLIB
      << std::flush;

   // Assigning a name to the temporary string object forces it to stay in
   // memory as long as its name go out of scope (this is what we need).
   const std::string&  gs = greetingString.str();

   // Allocate memory and copy greeting string
   s1 = gs.c_str();
   len = std::strlen(s1) + (std::size_t) 1;
   s2 = new char[len];
   std::memcpy(s2, s1, len);

   return(s2);
}


// =============================================================================
// UTF-8 to ISO 8859-1 conversion for window manager
// The caller is responsible to release the memory allocated for the result

#if CFG_USE_XSI && !CFG_NLS_DISABLE
static const char*  gui_utf8_iso(const char*  s)
{
   const char*  res = NULL;
   unsigned int  len;
   char*  tmp;

   // Check whether locale use UTF-8 (Decision based on 'LC_CTYPE')
   if(!fl_utf8locale())
   {
      // No => Expect that the WM can display ISO 8859-1
      //PRINT_ERROR("Non UTF-8 locale, converting string to ISO 8859-1");
      len = fl_utf8toa(s, (unsigned int) std::strlen(s), NULL, 0);
      tmp = new char[len + 1U];
      if(fl_utf8toa(s, (unsigned int) std::strlen(s), tmp, len + 1U) != len)
      {
         delete[] tmp;
      }
      else  { res = (const char*) tmp; }
   }
   else
   {
      // Yes => Return string unchanged
      len = (unsigned int) std::strlen(s);
      tmp = new char[len + 1U];
      std::strncpy(tmp, s, len + 1U);
      res = (const char*) tmp;
   }
   // Return NULL if conversion has failed or is not supported
   return(res);
}
#endif  // CFG_USE_XSI && !CFG_NLS_DISABLE


// =============================================================================
// Store content of FLTK text buffer to file
// The encoding of the pathname is converted to the encoding of the locale
// Returns zero on success


static int  gui_save_to_file(const char*  pathname, Fl_Text_Buffer*  tbuf)
{
   int  rv = -1;
   char*  s = tbuf->text();

   if(NULL != s)
   {
      rv = core_save_to_file(pathname, s);
      std::free((void*) s);
   }

   return(rv);
}


// =============================================================================
// Check whether article is suitable for display.
//
// FLTK 1.3 has problems with very long lines. The lines are not rendered
// correctly (so that the horizontal scrollbar can be used). There are reports
// that displaying such text have crashed the whole application.
// This bug is fixed in FLTK 1.4.
//
// This function should check for problematic content and replace the data in
// the text buffer tbuf with an appropriate error message.

static void  gui_check_article(Fl_Text_Buffer*  tbuf)
{
   int error = 0;

#if defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
   // NOP
#else  // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
   // Search for lines longer than 1000 octets (including CRLF).
   int  len = tbuf->length();
   int  pos = 0;

   while(len > pos)
   {
      int eol = tbuf->line_end(pos);

      if(1000 <= eol - pos)
      {
         // Line length error detected
         error = 1;
         break;
      }

      pos = eol;
      // Skip newline
      if(INT_MAX > pos) { ++pos; }
   }
#endif  // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION

   if(error)
   {
      // The delimiter is required for font selection and citation
      tbuf->text(ENC_DELIMITER);
      tbuf->append("[Error]\n");
      tbuf->append(S("Invalid line length"));
   }
}


// =============================================================================
// Some FLTK backends always display SHY, others never display SHY.
// The first variant is correct in the sense of ISO 8859-1, but is not
// the intended behaviour of most users. The second variant is wrong,
// because Unicode specifies that a SHY at the end of a line (where a
// linebreak was inserted by rewrap algorithm) should be visible.
// This should give consistent presentation of Soft Hyphen (SHY):
// - Delete all SHY characters not at the end of a line
// - Replace SHY at the end of a line with Hyphen-Minus

static void  gui_process_shy(Fl_Text_Buffer*  tbuf)
{
   int  pos_shy = 0;  // Search for soft hyphen from this position
   int  rv, rv2;

   do
   {
      rv = tbuf->findchar_forward(pos_shy, 0x00ADU, &pos_shy);
      if(rv)
      {
         // Check next character
         rv2 = tbuf->next_char(pos_shy);
         if(0x000AU == tbuf->char_at(rv2))
         {
            // LF => Replace SHY with hyphen-minus (at end of line)
            tbuf->replace(pos_shy, rv2, "-");
         }
         else  { tbuf->remove(pos_shy, rv2); }
      }
   }
   while(rv);
}


// =============================================================================
// Check subject line for "Re: " and report position behind (for replies)
// The caller must insert a "Re: " before that position to create a reply.

static const char*  gui_check_re_prefix(const char*  subject)
{
   bool  prefix_replaced;  // Nonstandard subject prefix replaced with "Re: "

   if((std::size_t) 3 <= std::strlen(subject))
   {
      // To be more tolerant, also accept "Re:", "RE:", "RE: ", "re:", "re: ",
      // "AW:", "AW: ", "Aw:" and "Aw: "
      do
      {
         prefix_replaced = false;
         if( (!std::strncmp(subject, "Re:", 3) && ' ' != subject[3])
             || !std::strncmp(subject, "RE:", 3)
             || !std::strncmp(subject, "re:", 3)
             || !std::strncmp(subject, "AW:", 3)
             || !std::strncmp(subject, "Aw:", 3) )
         {
            PRINT_ERROR("Nonstandard subject prefix"
                        " replaced with \"Re: \"");
            subject = &subject[3];
            if(' ' == subject[0])  { subject = &subject[1]; }
            prefix_replaced = true;
         }
      }
      while(prefix_replaced);
      // Standard variant
      if(!std::strncmp(subject, "Re: ", 4))  { subject = &subject[4]; }
   }

   return(subject);
}


// =============================================================================
// Create comma separated list of newsgroups
// The names of the newsgroups must be passed as NULL-terminated 'array'.
// The caller is responsible to delete[] the memory for the result string

static const char*  gui_create_newsgroup_list(const char**  array)
{
   static const char  error[] = "[Error]";
   char*  res = NULL;
   std::size_t  size_max = (std::size_t) -1;
   std::size_t  i = 0;
   std::size_t  len = 0;
   std::size_t  j;

   // Calculate required memory size
   while(NULL != array[i])
   {
      if(UINT_MAX == i)  { break; }
      j = std::strlen(array[i]);
      if(size_max - len < j)  { break; }
      len += j;
      ++i;
   }
   if(!i || (size_max - len < i))
   {
      res = new char[std::strlen(error) + (size_t) 1];
      std::strcat(res, error);
   }
   else
   {
      // Additional memory for comma separators and NUL termination
      len += i;
      // Create newsgroup list
      res = new char[len];
      res[0] = 0;
      for(j = 0; i > j; ++j)
      {
          if(j)  { std::strcat(res, ","); }
          std::strcat(res, array[j]);
      }
      res[len - (std::size_t) 1] = 0;
   }

   return(res);
}


// =============================================================================
// Check for signature separator to be the last one in MIME entity

static int  gui_last_sig_separator(char*  buf)
{
   int  res = 1;
   char*  p;
   char*  q;

   // Attention: Overloaded and both prototypes different than in C!
   p = std::strstr(buf, "\n-- \n");
   if(NULL != p)
   {
      res = 0;
      // Check for MIME entity separator before next signature separator
      // Attention: Overloaded and both prototypes different than in C!
      q = std::strstr(buf, "\n________________________________________"
                      "_______________________________________|\n");
      if(NULL != q && p > q)  { res = 1; }
   }

   return(res);
}


// =============================================================================
// Add introduction line and cite content (for replies)
// The name of the cited author must be passed as 'ca'.
// The array with newsgroup names of cited article must be passed as 'nga'.

static void  gui_cite_content(Fl_Text_Buffer*  compText, const char*  ca,
                              const char**  nga)
{
   const char*  ngl = gui_create_newsgroup_list(nga);
   const char*  qm;  // Quote mark
   int  rv = 1;
   int  pos = 0;
   int  pos_found;
   bool  fl = true;  // Flag indicating first level citation
   unsigned int  c;
   unsigned int  next;
   const char*  intro;

   // Set quote mark style according to config file
   switch(config[CONF_QUOTESTYLE].val.i)
   {
      case 0:  { qm = ">";  break; }
      case 1:  { qm = "> ";  break; }
      default:
      {
         PRINT_ERROR("Quoting style configuration not supported");
         // Use default from old versions that can't be configured
         qm = "> ";
         break;
      }
   }
   // Quote original content
   if(compText->length())
   {
      while(compText->char_at(pos))
      {
         // Check for second level citation marks
         if((unsigned int) '>' == compText->char_at(pos))
         {
            fl = false;
         }
         // Add additional citation mark
         compText->insert(pos, qm);
         // Unify spacing between and after citation marks if configured
         if(config[CONF_QUOTEUNIFY].val.i)
         {
            if(!config[CONF_QUOTESTYLE].val.i)
            {
               do
               {
                  c = compText->char_at(pos);
                  next = compText->char_at(compText->next_char(pos));
                  if((unsigned int) '>' == c)
                  {
                     if((unsigned int) ' ' == next)
                     {
                        // Remove space between marks:  "> " => ">"
                        compText->remove(pos + 1, pos + 2);
                     }
                  }
                  pos = compText->next_char(pos);
                  next = compText->char_at(pos);
               }
               while((unsigned int) '>' == next || (unsigned int) ' ' == next);
            }
            else
            {
               do
               {
                  c = compText->char_at(pos);
                  next = compText->char_at(compText->next_char(pos));
                  if((unsigned int) '>' == c)
                  {
                     if((unsigned int) '>' == next)
                     {
                        // Add missing space between marks:  ">>" => "> >"
                        compText->insert(++pos, " ");
                     }
                     else if((unsigned int) '>' != next
                             && (unsigned int) ' ' != next)
                     {
                        // Missing space after last mark "> >" => "> > "
                        compText->insert(++pos, " ");
                     }
                  }
                  pos = compText->next_char(pos);
               }
               while((unsigned int) '>' == next || (unsigned int) ' ' == next);
            }
         }
         // Search next EOL
         rv = compText->findchar_forward(pos, (unsigned int) '\n',
                                         &pos_found);
         if(!rv)  { break; } else  { pos = ++pos_found; }
      }
   }
   // Prepend new introduction line
   if(fl)
   {
      // Insert empty line between intro lines and cited content
      compText->insert(0, "\n");  compText->insert(0, qm);
   }
   // Prepend new introduction line with cited authors name
   intro = core_get_introduction(ca, ngl);
   if(NULL == intro)  { compText->insert(0, "[Error] wrote:\n"); }
   else
   {
      compText->insert(0, "\n");
      compText->insert(0, intro);
   }
   core_free((void*) intro);
   delete[] ngl;
   // Insert empty line after citation
   compText->append("\n");
}


// =============================================================================
// Print some header fields of article
//
// The caller is responsible to free the memory allocated for the returned
// string (if the result is not NULL).

#define UI_HDR_FIELDS  (std::size_t) 14
#define UI_HDR_PAD(s, n)  { for(i = 0; i < n; ++i)  { s << " "; } }
static const char*  gui_print_header_fields(struct core_article_header*  h)
{
   static const char*  f[UI_HDR_FIELDS] =
   {
      S("Subject"),
      S("Date"),
      S("From"),
      S("Reply-To"),
      S("Newsgroups"),
      S("Followup-To"),
      S("Distribution"),
      S("Message-ID"),
      S("Supersedes"),
      S("Organization"),
      S("User-Agent"),
      S("Transfer-Encoding"),
      S("Content-Type"),
      S("References")
   };
   std::size_t  flen[UI_HDR_FIELDS];
   std::size_t  fpad[UI_HDR_FIELDS];
   std::ostringstream  hdrData;
   char*  s = NULL;
   std::size_t  len;
   char  date[20];
   int  rv;
   std::size_t  i;
   std::size_t  largest = 0;

   // Calculate length of largest header field ID and padding for the others
   for(i = 0; i < UI_HDR_FIELDS; ++i)
   {
      len = std::strlen(f[i]);
#if CFG_USE_XSI && !CFG_NLS_DISABLE
      // Attention: The header field names can be translated if NLS is enabled.
      // The resulting Unicode strings may have a different number of characters
      // than bytes!
      if(INT_MAX < len)  { len = INT_MAX; }
      rv = fl_utf_nb_char((const unsigned char*) f[i], (int) len);
      if(0 > rv)  { flen[i] = len; }
      else  { flen[i] = (std::size_t) rv; }
#else  // CFG_USE_XSI && !CFG_NLS_DISABLE
      // Not translated header field names are US-ASCII by definition
      flen[i] = len;
#endif  // CFG_USE_XSI && !CFG_NLS_DISABLE
   }
   for(i = 0; i < UI_HDR_FIELDS; ++i)
   {
      if(largest < flen[i])  { largest = flen[i]; }
   }
   for(i = 0; i < UI_HDR_FIELDS; ++i)  { fpad[i] = largest - flen[i]; }

   // Copy header field content data
   UI_HDR_PAD(hdrData, fpad[0]);
   hdrData << f[0] << ": " << h->subject << "\n";
   rv = enc_convert_posix_to_iso8601(date, h->date);
   if(!rv)
   {
      UI_HDR_PAD(hdrData, fpad[1]);
      hdrData << f[1] << ": " << date << "\n";
   }
   UI_HDR_PAD(hdrData, fpad[2]);
   hdrData << f[2] << ": " << h->from << "\n";
   if(NULL != h->reply2)
   {
      if(std::strcmp(h->from, h->reply2))
      {
         UI_HDR_PAD(hdrData, fpad[3]);
         hdrData << f[3] << ": " << h->reply2 << "\n";
      }
   }
   UI_HDR_PAD(hdrData, fpad[4]);
   hdrData << f[4] << ": ";
   i = 0;
   while(NULL != h->groups[i])
   {
      if(i)  { hdrData << ","; }
      hdrData << h->groups[i++];
   }
   hdrData << "\n";
   if(NULL != h->fup2)
   {
      UI_HDR_PAD(hdrData, fpad[5]);
      hdrData << f[5] << ": " << h->fup2 << "\n";
   }

   if(NULL != h->dist)
   {
      UI_HDR_PAD(hdrData, fpad[6]);
      hdrData << f[6] << ": " << h->dist << "\n";
   }

   UI_HDR_PAD(hdrData, fpad[7]);
   hdrData << f[7] << ": " << h->msgid << "\n";
   if(NULL != h->supers)
   {
      UI_HDR_PAD(hdrData, fpad[8]);
      hdrData << f[8] << ": " << h->supers << "\n";
   }
   if(NULL != h->org)
   {
      UI_HDR_PAD(hdrData, fpad[9]);
      hdrData << f[9] << ": " << h->org << "\n";
   }
   if(NULL != h->uagent)
   {
      UI_HDR_PAD(hdrData, fpad[10]);
      hdrData << f[10] << ": " << h->uagent << "\n";
   }
   else if(NULL != h->x_newsr)
   {
      UI_HDR_PAD(hdrData, fpad[10]);
      hdrData << f[10] << ": " << h->x_newsr << "\n";
   }
   else if(NULL != h->x_pagent)
   {
      UI_HDR_PAD(hdrData, fpad[10]);
      hdrData << f[10] << ": " << h->x_pagent << "\n";
   }
   else if(NULL != h->x_mailer)
   {
      UI_HDR_PAD(hdrData, fpad[10]);
      hdrData << f[10] << ": " << h->x_mailer << "\n";
   }
   if(NULL != h->mime_cte)
   {
      UI_HDR_PAD(hdrData, fpad[11]);
      hdrData << f[11] << ": " << h->mime_cte << "\n";
   }
   if(NULL != h->mime_ct)
   {
      UI_HDR_PAD(hdrData, fpad[12]);
      hdrData << f[12] << ": " << h->mime_ct << "\n";
   }
   if(NULL != h->refs)
   {
      UI_HDR_PAD(hdrData, fpad[13]);
      // The GS control character marks the beginning of the reference list
      // The syntax highlighting code should replace it with SP later
      hdrData << f[13] << ":" << "\x1D";
      i = 0;
      while(NULL != h->refs[i])  { hdrData << "[" << i++ << "]"; }
      hdrData << "\n";
   }
   hdrData << std::flush;

   // Attention:
   //
   //    const char*  hs = hdrData.str().c_str()
   //
   // creates 'hs' with undefined value because the compiler is allowed to
   // free the temporary string object returned by 'str()' immediately after
   // the assignment!
   // Assigning names to temporary string objects forces them to stay in
   // memory as long as their names go out of scope (this is what we need).
   const std::string&  hs = hdrData.str();

   len = std::strlen(hs.c_str());
   s = new char[++len];
   std::strncpy(s, hs.c_str(), len);

   return(s);
}


// =============================================================================
// Create RFC 8089 conformant link to save entity to a file
//
// We use a reserved subdirectory in CFG_NAME to avoid clashes with links
// created by the author of the article.
//
// The parameter msgid must point to a string with a valid Message-ID.
//
// The caller is responsible to free() the memory allocated for the link if
// the result is not NULL.

static const char*  gui_create_link_to_entity(const char*  msgid,
                                              std::size_t  entity)
{
   const char*  link = NULL;
   Fl_Text_Buffer  tb;
   const char*  confdir = NULL;
   char*  msgid_buf;
   std::size_t  len;
   std::ostringstream  entity_no;

   confdir = xdg_get_confdir(CFG_NAME);
   if(NULL != confdir)
   {
      tb.text("<file://");
      tb.append(confdir);
      tb.append("/.mime/entities/");
      // Append Message-ID without angle brackets
      len = std::strlen(msgid);
      msgid_buf = new char[len];
      std::strncpy(msgid_buf, &msgid[1], --len);
      msgid_buf[--len] = 0;
      tb.append(msgid_buf);
      delete[] msgid_buf;
      tb.append("/");
      // Append entity index
      // Note: We cannot use 'posix_snprintf()' here
      entity_no.str(std::string());
      entity_no << entity << std::flush;
      const std::string&  s = entity_no.str();
      tb.append(s.c_str());
      // Append closing delimiter
      tb.append(">");
      // Copy complete link
      link = tb.text();
   }
   core_free((void*) confdir);

   return(link);
}


// =============================================================================
// Decode MIME entities into FLTK text buffer
//
// The parameter tb must be a valid pointer to a FLTK text buffer.
// The parameter mimeData is allowed to be NULL.

static void  gui_decode_mime_entities(Fl_Text_Buffer  *tb,
                                      MIMEContent*  mimeData,
                                      const char*  msgid)
{
   const char*  p = NULL;
   const char*  q = NULL;
   std::size_t  i;
   std::size_t  first;
   std::size_t  num;
   enc_mime_cte  cte = ENC_CTE_UNKNOWN;
   enc_mime_ct*  ct = NULL;
   const char*  header;
   const char*  body;
   char  last_char;
   int  attachment;

   if(NULL == mimeData)
   {
      tb->append(S("MIME content handling error"));
   }
   else
   {
      first = 0;
      num = mimeData->parts();
      // Append MIME entities to text buffer
      for(i = first; i < num; ++i)
      {
         body = mimeData->part(i, &cte, &ct);
         // Print delimiter between entities
         if(i)
         {
            // Append a linefeed if last entity doesn't has one at its end
            last_char = tb->byte_at(tb->length() - 1);
            if((char) 0x0A != last_char)  { tb->append("\n"); }
            tb->append(ENC_DELIMITER);
         }
         // Print own headers for multipart entities
         if(mimeData->is_multipart())
         {
            header = mimeData->part_header(i);
            if(NULL != header)
            {
               tb->append(header);
               tb->append("\n");
            }
         }
         // Check content disposition
         attachment = 0;
         if(ENC_CD_ATTACHMENT == mimeData->type(i))  { attachment = 1; }
         if(attachment)
         {
            // Respect content disposition declaration "attachment"
            tb->append(S("Declared as attachment by sender"));
            // Display RFC 8089 conformant link to save entity to a file.
            tb->append("\n");
            p = gui_create_link_to_entity(msgid, i);
            if(NULL == p)  { tb->append("[Error]"); }
            else  { tb->append(p); }
            std::free((void*) p);
         }
         // RFC 2049 requires that unknown subtypes of "text" must be
         // viewable raw/undecoded => We handle them as subtype "plain"
         // (print them undecoded)
         else if(ENC_CT_TEXT != ct->type && ENC_CT_MESSAGE != ct->type)
         {
            // Error: Content is not supported
            tb->append(S("MIME content type not supported"));
            // Display RFC 8089 conformant link to save entity to a file.
            tb->append("\n");
            p = gui_create_link_to_entity(msgid, i);
            if(NULL == p)  { tb->append("[Error]"); }
            else  { tb->append(p); }
            std::free((void*) p);
         }
         else
         {
            // Decode 'text/plain' content and convert it to Unicode
            p = enc_mime_decode(cte, ct->charset, body);
            if(NULL == p)
            {
               tb->append(S("MIME content decoding failed"));
            }
            else
            {
               // Handle content with "Format=Flowed" attribute
               if(ct->flags & ENC_CT_FLAG_FLOWED)
               {
                  q = enc_mime_flowed_decode(p,
                      ct->flags & ENC_CT_FLAG_DELSP,
                      ct->flags & ENC_CT_FLAG_INSLINE);
                  if(NULL == q)
                  {
                     PRINT_ERROR("Decoding of MIME Format=Flowed"
                                 " content failed");
                  }
                  else
                  {
                     if(p != q && p != body)  { enc_free((void*) p); }
                     p = q;
                  }
               }
               // Convert article content line breaks from canonical
               // (RFC 822) form to POSIX form
               q = core_convert_canonical_to_posix(p, 1, 0);
               if(NULL == q)
               {
                  tb->append(
                     S("Conversion from canonical to local form failed"));
               }
               else
               {
                  // Free memory allocated by MIME decoder (if any)
                  if(p != q && p != body)  { enc_free((void*) p); }
                  p = q;
                  // Success
                  tb->append(p);
                  if(p != body)  { enc_free((void*) p); }
               }
            }
         }
      }
   }
}


// =============================================================================
// Check whether file exists and ask user if OK to overwrite

static int  gui_check_pathname(const char*  pathname)
{
   int  res = 0;
   int  rv;

   rv = core_check_file_exist(pathname);
   if (0 == rv)
   {
      // File exists
      fl_message_title(S("Warning"));
      rv = !fl_choice("%s", S("Cancel"),
                      S("OK"), NULL,
                      S("File exists\nReally continue?"));
      if(rv)
      {
         // User does not want to overwrite the file
         res = -1;
      }
   }

   return(res);
}


// =============================================================================
// Populate group list

static int  gui_populate_group_list(const char*  grouplist)
{
   int  res = 0;
   std::size_t  len;
   char*  buf;
   int  rv;
   int  pos = 0;
   int  pos_new;

   if(NULL != grouplist)
   {
      len = std::strlen(grouplist);
      if(len)
      {
         buf = new char[len + (std::size_t) 1];
         while(1)
         {
            rv = std::sscanf(&grouplist[pos], "%s%n", buf, &pos_new);
            if (1 != rv || !pos_new)  { break; }
            else
            {
               pos += pos_new;
               (void) core_subscribe_group(buf);
               // Ignore potential error and continue with next group
            }
         }
         delete[] buf;
      }
   }

   return(res);
}


// =============================================================================
// My_Multi_Browser event handler

int  My_Multi_Browser::handle(int  event)
{
   int  res = 0;

   if(FL_KEYUP == event)
   {
      // FLTK 1.4.0 does not draw the focus rectangle correctly
      if(Fl::event_key() == FL_Up)
      {
         redraw();
         res = 1;
      }
   }
   else if(FL_KEYDOWN == event)
   {
      if(Fl::event_key() == FL_Enter)
      {
         void*  sel = selection();

         if (NULL != sel)
         {
            deselect();
            select_only(sel, 1);
            res = 1;
         }
      }
      // Space key
      else if(Fl::event_key() == 0x20)
      {
         // Ignore
         res = 1;
      }
   }

   // Other events go to the default handler
   if(!res)  { res = Fl_Multi_Browser::handle(event); }

   return(res);
}


// =============================================================================
// My_Text_Display event handler

int  My_Text_Display::handle(int  event)
{
   int  res = 0;
   int  button;
   int  x, y;
   int  pos;
   unsigned int  style;

   switch(event)
   {
      case FL_KEYDOWN:
      {
         // Space key pressed
         if(Fl::event_key() == 0x20)
         {
            // Scroll down
            mainWindow->ascrolldown_cb(true);
            res = 1;
         }
         // Slash key pressed
         else if(!std::strcmp(Fl::event_text(), "/"))
         {
            mainWindow->asearch_cb_i();
            res = 1;
         }
         break;
      }
      case FL_MOVE:
      {
         // Mouse has moved inside the widget
         if(NULL != mStyleBuffer)
         {
            x = Fl::event_x();
            y = Fl::event_y();
            pos = xy_to_position(x, y);
            style = mStyleBuffer->char_at(pos);
            if(NULL != mainWindow)
            {
               if(style == mainWindow->hyperlinkStyle)
               {
                  // Mouse over hyperlink => Change mouse pointer to 'hand'
                  fl_cursor(FL_CURSOR_HAND);
               }
               else  { fl_cursor(FL_CURSOR_DEFAULT); }
               res = 1;
            }
         }
         break;
      }
      case FL_PUSH:
      case FL_RELEASE:
      {
         // Mouse button pressed or released
         if(NULL != mStyleBuffer)
         {
            button = Fl::event_button();
            if(FL_LEFT_MOUSE == button)
            {
               // Left mouse button detected
               x = Fl::event_x();
               y = Fl::event_y();
               pos = xy_to_position(x, y);
               style = mStyleBuffer->char_at(pos);
               // Check whether click hit hyperlink
               if(NULL != mainWindow)
               {
                  if(style == mainWindow->hyperlinkStyle)
                  {
                     if(FL_PUSH == event)  { linkPushed = pos; }
                     else if(pos == linkPushed)
                     {
                        // Click on hyperlink detected => Execute callback
                        // std::printf("Click on hyperlink detected\n");
                        linkPushed = -1;
                        mainWindow->hyperlinkPosition = pos;
                        res = 1;
                        do_callback();
                        break;
                     }
                  }
               }
            }
            if(FL_RELEASE == event)  { linkPushed = -1; }
         }
         break;
      }
      default:
      {
         break;
      }
   }
   // Other events go to the default handler
   if(!res)  { res = Fl_Text_Display::handle(event); }

   return(res);
}


// =============================================================================
// My_Tree event handler
//
// Since 1.3.1 the old Fl_Tree widget provides a 'get_item_focus()' method.
// Since 1.3.3 the new Fl_Tree widget provides a 'next_visible_item()' method.

int  My_Tree::handle(int  event)
{
   int  res = 0;

   // Consume and ignore events while update of widget is in progress
   if(update_in_progress())
   {
      res = 1;
   }
   // Intercept key pressed events
   else switch(event)
   {
      case FL_KEYDOWN:
      {
         // Down (cursor) key
#ifdef FL_ABI_VERSION
#  if 10303 <= FL_ABI_VERSION
         // Requires FLTK 1.3.3
         if(Fl::event_key() == FL_Down)
         {
            Fl_Tree_Item*  ti = get_item_focus();
            if(NULL != ti && NULL == next_visible_item(ti, FL_Down))
            {
               res = 1;
               break;
            }
         }
         else
#  endif  // 10303 <= FL_ABI_VERSION
#endif  // FL_ABI_VERSION
         // Enter key
         if(Fl::event_key() == FL_Enter)
         {
#ifdef FL_ABI_VERSION
#  if 10301 <= FL_ABI_VERSION
            // Requires FLTK 1.3.1
            // Looks like there is no safe workaround to emulate with 1.3.0
            Fl_Tree_Item*  ti = first_selected_item();
            if(NULL != ti)  { deselect(ti); }
            select(get_item_focus(), 1);
#  endif  // 10301 <= FL_ABI_VERSION
#endif  // FL_ABI_VERSION
            res = 1;
            break;
         }
         // Space key
         else if(Fl::event_key() == 0x20)
         {
            mainWindow->ascrolldown_cb(true);
            res = 1;
            break;
         }
         // Slash key pressed
         else if(!std::strcmp(Fl::event_text(), "/"))
         {
            mainWindow->asearch_cb_i();
            res = 1;
            break;
         }
         // No 'break' here is intended, use default handler if ignored
      }
      // FALLTHROUGH
      default:
      {
         res = Fl_Tree::handle(event);
         break;
      }
   }

   return(res);
}


// =============================================================================
// Main window callback state machine
//
// This function is intended to check whether an operation is currently allowed
// to execute.
//
// Every operation that use shared ressources (like the core thread) must call
// this function before start of execution with \e operation set to a unique
// operation ID associated with this operation.
// This function returns \c true (and change internal state) if starting
// execution of this operation is allowed at the moment, otherwise the operation
// must not be started (and the internal state is preserved).
//
// Some operations call this function again with a different operation ID to
// indicate completion (such indications are always accepted by this function).

bool  MainWindow::stateMachine(enum mainWindowEvent  operation)
{
   bool  res = true;

   switch(mainState)
   {
      case STATE_READY:
      {
         switch(operation)
         {
            case EVENT_SUBSCRIBE:
            case EVENT_GL_REFRESH:
            case EVENT_A_VIEW:
            case EVENT_A_MAAR:
            case EVENT_A_MAGAR:
            case EVENT_SRC_VIEW:
            case EVENT_MOTD_VIEW:
            {
               mainState = STATE_MUTEX;
               break;
            }
            case EVENT_SERVER:
            {
               mainState = STATE_SERVER1;
               break;
            }
            case EVENT_G_SELECT:
            {
               mainState = STATE_GROUP;
               break;
            }
            case EVENT_A_PREPARE:
            {
               mainState = STATE_NEXT;
               break;
            }
            case EVENT_SCROLL_NEXT:
            {
               mainState = STATE_SCROLL;
               break;
            }
            case EVENT_COMPOSE:
            {
               mainState = STATE_COMPOSE;
               break;
            }
            case EVENT_SERVER_EXIT:
            case EVENT_G_SELECT_EXIT:
            case EVENT_GL_REFRESH_EXIT:
            case EVENT_SCROLL_NEXT_EXIT:
            {
               // Ignore
               break;
            }
            default:
            {
               // Reject all other operations
               res = false;
               break;
            }
         }
         break;
      }
      case STATE_MUTEX:
      {
         switch(operation)
         {
            case EVENT_SUBSCRIBE_EXIT:
            case EVENT_GL_REFRESH_EXIT:
            case EVENT_A_VIEW_EXIT:
            case EVENT_A_MAAR_EXIT:
            case EVENT_A_MAGAR_EXIT:
            case EVENT_SRC_VIEW_EXIT:
            case EVENT_MOTD_VIEW_EXIT:
            {
               mainState = STATE_READY;
               break;
            }
            default:
            {
               // Reject all other operations
               res = false;
               break;
            }
         }
         break;
      }
      case STATE_SERVER1:
      {
         switch(operation)
         {
            case EVENT_GL_PROPOSAL:
            {
               mainState = STATE_PROPOSAL;
               break;
            }
            case EVENT_GL_REFRESH:
            {
               mainState = STATE_SERVER2;
               break;
            }
            case EVENT_SERVER_EXIT:
            {
               mainState = STATE_READY;
               break;
            }
            default:
            {
               // Reject all other operations
               res = false;
               break;
            }
         }
         break;
      }
      case STATE_SERVER2:
      {
         switch(operation)
         {
            case EVENT_SERVER_EXIT:
            case EVENT_GL_PROPOSAL_EXIT:
            {
               // Ignore
               break;
            }
            case EVENT_GL_REFRESH_EXIT:
            {
               mainState = STATE_READY;
               break;
            }
            default:
            {
               // Reject all other operations
               res = false;
               break;
            }
         }
         break;
      }
      case STATE_PROPOSAL:
      {
         switch(operation)
         {
            case EVENT_SERVER_EXIT:
            {
               // Ignore
               break;
            }
            case EVENT_GL_PROPOSAL_EXIT:
            {
               mainState = STATE_READY;
               break;
            }
            case EVENT_GL_REFRESH:
            {
               mainState = STATE_SERVER2;
               break;
            }
            default:
            {
               // Reject all other operations
               res = false;
               break;
            }
         }
         break;
      }
      case STATE_GROUP:
      {
         switch(operation)
         {
            case EVENT_G_SELECT_EXIT:
            case EVENT_AT_REFRESH:
            {
               // Preserve state
               break;
            }
            case EVENT_AT_REFRESH_EXIT:
            {
               mainState = STATE_READY;
               break;
            }
            default:
            {
               // Reject all other operations
               res = false;
               break;
            }
         }
         break;
      }
      case STATE_SCROLL:
      {
         switch(operation)
         {
            case EVENT_A_PREPARE:
            {
               mainState = STATE_NEXT;
               break;
            }
            case EVENT_SCROLL_NEXT_EXIT:
            {
               mainState = STATE_READY;
               break;
            }
            default:
            {
               // Reject all other operations
               res = false;
               break;
            }
         }
         break;
      }
      case STATE_NEXT:
      {
         switch(operation)
         {
            case EVENT_A_SELECT:
            case EVENT_SCROLL_NEXT_EXIT:
            {
               // Preserve state
               break;
            }
            case EVENT_A_SELECT_EXIT:
            {
               mainState = STATE_READY;
               break;
            }
            default:
            {
               // Reject all other operations
               res = false;
               break;
            }
         }
         break;
      }
      case STATE_COMPOSE:
      {
         switch(operation)
         {
            case EVENT_POST:
            {
               mainState = STATE_POST;
               break;
            }
            case EVENT_COMPOSE_EXIT:
            {
               mainState = STATE_READY;
               break;
            }
            default:
            {
               // Reject all other operations
               res = false;
               break;
            }
         }
         break;
      }
      case STATE_POST:
      {
         switch(operation)
         {
            case EVENT_POST_EXIT:
            {
               mainState = STATE_COMPOSE;
               break;
            }
            default:
            {
               // Reject all other operations
               res = false;
               break;
            }
         }
         break;
      }
      default:
      {
         // This should never be executed, try to recover
         PRINT_ERROR("Invalid state detected by main window state machine");
         mainState = STATE_READY;
         res = false;
         break;
      }
   }

   return(res);
}


// =============================================================================
// Main Window exit callback

void  MainWindow::exit_cb_i(void)
{
   // Ignore escape key
   if(Fl::event() == FL_SHORTCUT && Fl::event_key() == FL_Escape)
   {
      return;
   }

   // Schedule exit request
   exitRequest = 1;
}


// =============================================================================
// Main Window print callback

void  MainWindow::print_cb_i(void)
{
   const Fl_Font  font = FL_COURIER;
   const Fl_Fontsize  fontsize = 14;
   const char  line[] = "________________________________________"
                        "________________________________________";
   int  old_scrollbar_size = Fl::scrollbar_size();
   int  res = 0;
   Fl_Printer  printer;
   Fl_Text_Buffer  tb;
   Fl_Text_Buffer  sb;
   Fl_Text_Display  canvas(0, 0, 100, 100);
   int  lines;
   int  pages = 1;
   int  frompage, topage;
   int  w;  // Canvas width in points
   int  h = 0;  // Canvas height in points
   int  lh;  // Line hight in points
   int  lpp = 0;  // Lines per page
   int  pw, ph;  // Printable area size in points
   float  sf;  // Scale factor
   float  tmp;
   int  i;
   char*  p;
   int  x, y;
   int  current_line = 1;  // The first line is number 1
   int  rv;

   if(NULL == currentGroup)
   {
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s", S("No group selected"));
   }
   else if(NULL == currentArticleHE)
   {
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s", S("No article selected"));
   }
   else
   {
      // Calculate width for 80 columns and height for ISO 216 A4 paper
      fl_font(font, fontsize);
      fl_measure(line, w = 0, lh = 0);
      if(0 >= w || 0 >= lh)  { res = -1; }
      else  { h = (int) std::ceil((double) w * std::sqrt(2.0)); }
      if(!res)
      {
         // Prepare text buffer
         p = text->buffer()->text();
         if(NULL != p)  { tb.text(p); }
         std::free((void*) p);
         lines = tb.count_lines(0, tb.length());
         if(main_debug)
         {
            printf("%s: %sPrinting article with %d lines\n",
                   CFG_NAME, MAIN_ERR_PREFIX, lines);
         }
         if (0 >= lines)  { res = -1; }
      }
      if(!res)
      {
         // Prepare style buffer
         p = currentStyle->text();
         if(NULL != p)  { sb.text(p); }
         std::free((void*) p);
         // Prepare canvas
         w += lh;
         h += lh;
         canvas.box(FL_NO_BOX);
         canvas.textfont(font);
         canvas.textsize(fontsize);
         canvas.resize(0, 0, w, h);
         canvas.buffer(&tb);
         canvas.highlight_data(&sb, styles, styles_len, 'A', NULL, NULL);
         // Calculate number of pages
         i = 0;
         while(canvas.position_to_xy(i, &x, &y))
         {
            if(!canvas.move_down())  { break; }
            i = canvas.insert_position();
            ++lpp;
         }
         if(lines > lpp)
         {
            // One page is not sufficient
            // Remove one line because the last one is maybe only partly visible
            --lpp;
            if(0 >= lpp)  { res = -1; }
         }
         if(main_debug)
         {
            printf("%s: %sPrinting %d lines per page\n",
                   CFG_NAME, MAIN_ERR_PREFIX, lpp);
         }
         // Calculate lines per page
         if(!res)
         {
            for(i = 0; i < lpp; ++i)  { tb.append("\n"); }  // For scrolling
            pages = lines / lpp;
            if(lines % lpp)  { ++pages; }
            if(1 > pages)  { res = -1; }
         }
         if(main_debug)
         {
            printf("%s: %sPrinting %d pages\n",
                   CFG_NAME, MAIN_ERR_PREFIX, pages);
         }
      }
      // Setup printer
      if(!res)
      {
         res = printer.start_job(pages, &frompage, &topage);
         if(!res)
         {
            // Scrollbars of canvas are printed without this
            Fl::scrollbar_size(0);
            for(i = 1; i <= pages; ++i)
            {
               // Scroll down to next page
               canvas.scroll(current_line, 0);
               // Check whether page was selected
               if(i >= frompage && i <= topage)
               {
                  // Scale page to printable area
                  res = printer.start_page();
                  if(!res)
                  {
                     res = printer.printable_rect(&pw, &ph);
                     if(!res)
                     {
                        sf = (float) pw / (float) w;
                        tmp = (float) ph / (float) h;
                        if(std::fabs(tmp) < std::fabs(sf))  { sf = tmp; }
                        // Scaling sets origin too
                        printer.scale(sf);
                        // Print page
                        printer.print_widget((Fl_Widget*) &canvas);
                     }
                     rv = printer.end_page();
                     if(!res)  { res = rv; }
                  }
               }
               // Check for error
               if(res)  { break; }
               else
               {
                  // Calculate vertical offset for next page
                  current_line += lpp;
               }
            }
            printer.end_job();
            Fl::scrollbar_size(old_scrollbar_size);
         }
      }
   }

   if(res)
   {
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s", S("Printing failed or aborted"));
   }
}


// =============================================================================
// Main Window save cooked article (to file, UTF-8 encoded) callback

void  MainWindow::asave_cb_i(void)
{
   const char*  pathname;
   int  rv;

   if(NULL == currentGroup)
   {
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s", S("No group selected"));
   }
   else if(NULL == currentArticleHE)
   {
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s", S("No article selected"));
   }
   else
   {
      SC("Do not use characters for the translation that cannot be")
      SC("converted to the ISO 8859-1 character set for this item.")
      SC("Leave the original string in place if in doubt.")
      const char*  title = S("Save article");
      const char*  suggested_pathname = core_suggest_pathname();

      fl_file_chooser_ok_label(S("Save"));
      if(NULL != suggested_pathname)
      {
         pathname = fl_file_chooser(title, "*", suggested_pathname, 0);
         if(NULL != pathname)
         {
            // Ask user for overwrite permission if file exists
            rv = gui_check_pathname(pathname);
            if (0 == rv)
            {
               rv = gui_save_to_file(pathname, currentArticle);
               if(rv)
               {
                  SC("Do not use non-ASCII for the translation of this item")
                  fl_message_title(S("Error"));
                  fl_alert("%s", S("Operation failed"));
               }
            }
         }
         core_free((void*) suggested_pathname);
      }
   }
}


// =============================================================================
// Main Window search in article content

void  MainWindow::asearch_cb_i(void)
{
   SearchWindow*  sw = NULL;
   int  result = -1;
   int  found = 0;
   int  found_pos = 0;
   std::size_t  tmp, tmp2;
   char*  p;
   int  start, end, len;
   int  sel_start, sel_end;
   SC("")
   SC("Do not use characters for the translation that cannot be converted to")
   SC("the ISO 8859-1 character set for this item.")
   SC("Leave the original string in place if in doubt.")
   const char*  titleString = S("Search in article");
   SC("")

   if(NULL == currentArticleHE)
   {
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s", S("No article selected"));
   }
   else
   {
      // Display "Search in article" window
#if CFG_USE_XSI && !CFG_NLS_DISABLE
      // Convert window title to the encoding required by the window manager (WM)
      // It is assumed that the WM can display either ISO 8859-1 or UTF-8 encoded
      // window titles.
      const char*  title;
      title = gui_utf8_iso(titleString);
      if(NULL != title)
      {
         sw = new SearchWindow(title, &currentSearchString);
         delete[] title;
      }
#else  // CFG_USE_XSI && !CFG_NLS_DISABLE
      sw = new SearchWindow(titleString, &currentSearchString);
#endif  // CFG_USE_XSI && !CFG_NLS_DISABLE

      // Wait for user to press OK or Cancel
search_again:
      if(NULL != sw)
      {
         sw->default_cursor(FL_CURSOR_DEFAULT);
         UI_READY();
         sw->finished = 0;
         while(!sw->finished)  { Fl::wait(); }
         sw->default_cursor(FL_CURSOR_WAIT);
         result = sw->finished;
      }

      // Check result
      currentArticle->unhighlight();
      if(0 > result)
      {
         // Search cancelled by user
         UI_STATUS(S("Search cancelled, reset start position."));
         currentSearchPosition = 0;
      }
      else
      {
         // Search in current article
         UI_STATUS(S("Searching in article ..."));
         UI_BUSY();
         Fl::check();
         if(config[CONF_SEARCH_CASE_IS].val.i)
         {
            // Case insensitive (using FLTK will not be Unicode conformant)
            p = currentArticle->text();
            found = !enc_uc_search(p, (size_t) currentSearchPosition,
                                   currentSearchString, &tmp, &tmp2);
            if((std::size_t) INT_MAX <= tmp)  { found_pos = INT_MAX; }
            else { found_pos = (int) tmp; }
            if((std::size_t) INT_MAX <= tmp2)  { len = INT_MAX; }
            else { len = (int) tmp2; }
            std::free((void*) p);
         }
         else
         {
            // Case sensitive (using FLTK)
            found = currentArticle->search_forward(currentSearchPosition,
                                                   currentSearchString,
                                                   &found_pos, 1);
            // Length was already checked for INT_MAX overflow by SearchWindow
            len = (int) std::strlen(currentSearchString);
         }
         if(found)
         {
            // Highlight found text
            start = found_pos;
            if(INT_MAX - start < len)  { len = INT_MAX - start; }
            end = start + len;
            currentSearchPosition = end;
            currentArticle->highlight(start, end);
            UI_STATUS(S("Search string found and marked, position stored."));
            // Scroll vertically to bring found position into view
            //if(currentArticle->selection_position(&sel_start, &sel_end))
            if(currentArticle->highlight_position(&sel_start, &sel_end))
            {
               // Now: Horizontal scrolling doesn't work correctly without this
               text->insert_position(0);
               text->show_insert_position();
               text->insert_position(sel_end);
               text->show_insert_position();
            }
         }
         else
         {
            UI_STATUS(S("Search string not found, reset start position."));
            // Reset start position
            currentSearchPosition = 0;
         }
         // Return to search window
         goto search_again;
      }

      // Destroy search window
      if(NULL != sw)  { delete sw; }
      UI_READY();
   }
}


// =============================================================================
// Main window calculate percent value and label for progress bar

void  MainWindow::calculatePercent(std::size_t  current, std::size_t  complete)
{
   std::ostringstream  percentString;

   progress_percent_value = 100.0;
   progress_percent_label[0] = '1';
   progress_percent_label[1] = '0';
   progress_percent_label[2] = '0';
   progress_percent_label[3] = '%';
   if(complete && (current < complete))
   {
      progress_percent_value = (float) current / (float) complete;
      progress_percent_value *= (float) 100.0;
      percentString.precision(0);
      percentString << std::fixed << progress_percent_value << "%"
                    << std::flush;

      // Attention:
      //
      //    const char*  s = percentString.str().c_str()
      //
      // creates 's' with undefined value because the compiler is allowed to
      // free the temporary string object returned by 'str()' immediately after
      // the assignment!
      // Assigning names to temporary string objects forces them to stay in
      // memory as long as their names go out of scope (this is what we need).
      const std::string&  ps = percentString.str();

      std::strncpy(progress_percent_label, ps.c_str(), (std::size_t) 4);
   }
   // Terminate string
   progress_percent_label[4] = 0;
}


// =============================================================================
// Main Window configuration callback

void  MainWindow::config_cb_i(void)
{
   SC("")
   SC("Do not use characters for the translation that cannot be converted to")
   SC("the ISO 8859-1 character set for this item.")
   SC("Leave the original string in place if in doubt.")
   const char*  titleString = S("Configuration");
   SC("")

   // Display "Configuration" window
#if CFG_USE_XSI && !CFG_NLS_DISABLE
   // Convert window title to the encoding required by the window manager (WM)
   // It is assumed that the WM can display either ISO 8859-1 or UTF-8 encoded
   // window titles.
   const char*  title;
   title = gui_utf8_iso(titleString);
   if(NULL != title)
   {
      new MiscCfgWindow(title);
      delete[] title;
   }
#else  // CFG_USE_XSI && !CFG_NLS_DISABLE
   new MiscCfgWindow(titleString);
#endif  // CFG_USE_XSI && !CFG_NLS_DISABLE
}


// =============================================================================
// Main Window identity configuration callback

void  MainWindow::identity_cb_i(void)
{
   SC("")
   SC("Do not use characters for the translation that cannot be converted to")
   SC("the ISO 8859-1 character set for this item.")
   SC("Leave the original string in place if in doubt.")
   const char*  titleString = S("Identity configuration");
   SC("")

   // Display "Identity configuration" window
#if CFG_USE_XSI && !CFG_NLS_DISABLE
   // Convert window title to the encoding required by the window manager (WM)
   // It is assumed that the WM can display either ISO 8859-1 or UTF-8 encoded
   // window titles.
   const char*  title;
   title = gui_utf8_iso(titleString);
   if(NULL != title)
   {
      new IdentityCfgWindow(title);
      delete[] title;
   }
#else  // CFG_USE_XSI && !CFG_NLS_DISABLE
   new IdentityCfgWindow(titleString);
#endif  // CFG_USE_XSI && !CFG_NLS_DISABLE
}


// =============================================================================
// Main Window Message-ID search callback

void  MainWindow::mid_search_cb_i(void)
{
   SC("")
   SC("Do not use characters for the translation that cannot be converted to")
   SC("the ISO 8859-1 character set for this item.")
   SC("Leave the original string in place if in doubt.")
   const char*  titleString = S("Message-ID search");
   SC("")

   // Display "Message-ID search" window
#if CFG_USE_XSI && !CFG_NLS_DISABLE
   // Convert window title to the encoding required by the window manager (WM)
   // It is assumed that the WM can display either ISO 8859-1 or UTF-8 encoded
   // window titles.
   const char*  title;
   title = gui_utf8_iso(titleString);
   if(NULL != title)
   {
      new MIDSearchWindow(title);
      delete[] title;
   }
#else  // CFG_USE_XSI && !CFG_NLS_DISABLE
   new MIDSearchWindow(titleString);
#endif  // CFG_USE_XSI && !CFG_NLS_DISABLE
}


// =============================================================================
// Main Window about information callback

void  MainWindow::about_cb_i(void)
{
   std::ostringstream  titleString;

   // Create Unicode title for about window
   SC("")
   SC("Do not use characters for the translation that cannot be converted to")
   SC("the ISO 8859-1 character set for this item.")
   SC("Leave the original string in place if in doubt.")
   titleString << S("About") << " " << CFG_NAME << std::flush;
   SC("")

   // Attention:
   //
   //    const char*  title = titleString.str().c_str()
   //
   // creates 'title' with undefined value because the compiler is allowed to
   // free the temporary string object returned by 'str()' immediately after the
   // assignment!
   // Assigning names to temporary string objects forces them to stay in memory
   // as long as their names go out of scope (this is what we need).
   const std::string&  ts = titleString.str();
   const std::string&  as = aboutString.str();

   // Set "About" window title
#if CFG_USE_XSI && !CFG_NLS_DISABLE
   // Convert window title to the encoding required by the window manager (WM)
   // It is assumed that the WM can display either ISO 8859-1 or UTF-8 encoded
   // window titles.
   const char*  title;
   title = gui_utf8_iso(ts.c_str());
   if(NULL != title)
   {
      fl_message_title(title);
      delete[] title;
   }
#else  // CFG_USE_XSI && !CFG_NLS_DISABLE
   fl_message_title(ts.c_str());
#endif  // CFG_USE_XSI && !CFG_NLS_DISABLE

   // Display "About" window
   fl_message("%s", as.c_str());
}


// =============================================================================
// Main Window bug report callback

void  MainWindow::bug_cb_i()
{
   std::ostringstream  subjectString;
   std::ostringstream  configString;
   std::ostringstream  contentString;
   std::ostringstream  titleString;
   int  rv = -1;
   char*  maintainer;
   std::size_t  len;
   char*  p;
   char*  q;

   configString << "Configuration:" << "\n"
                << CFG_NAME << " " << CFG_VERSION
                << " " << "for" << " " << CFG_OS  << "\n"
#if CFG_MODIFIED
                << "(This is a modified version!)" << "\n"
#endif  // CFG_MODIFIED
                << "Unicode version: " << UC_VERSION << "\n"
                << "NLS: "
#if CFG_USE_XSI && !CFG_NLS_DISABLE
                << "Enabled" << "\n"
#else  // CFG_USE_XSI && !CFG_NLS_DISABLE
                << "Disabled" << "\n"
#endif  // CFG_USE_XSI && !CFG_NLS_DISABLE
                << "Message locale: " << nls_loc << "\n"
                << "TLS: "
#if CFG_USE_TLS
                << "Available" << "\n"
#  if CFG_USE_OPENSSL_API_1_1
                << "Compiled for OpenSSL API 1.1" << "\n"
#  endif  // !CFG_USE_OPENSSL_API_1_1
#else  // CFG_USE_TLS
                << "Not available" << "\n"
#endif  // CFG_USE_TLS
                << "Compiled for: FLTK " << FL_MAJOR_VERSION << "."
                << FL_MINOR_VERSION  << "." << FL_PATCH_VERSION << "\n"
#if CFG_CMPR_DISABLE
                << "Compression disabled" << "\n"
#else  // CFG_CMPR_DISABLE
                << "Compression available"
#  if CFG_USE_ZLIB
                << " (zlib)"
#  endif  // CFG_USE_ZLIB
                << "\n"
#endif  // CFG_CMPR_DISABLE
                << "Build: " << BDATE
                << "\n\n"
                << "Problem description (in english):" << "\n\n"
                << std::flush;
   subjectString << "[" << CFG_NAME << "] "
                 << "Bug report (...)"
                 << std::flush;
   contentString << S("Report bug to:") << "\n"
                 << CFG_MAINTAINER
                 << "\n\n"
                 << S("Use the following subject line and replace")
                 << " '...' " << S("with a summary") << ":\n"
                 << subjectString.str() << "\n\n"
                 << S("Use the following skeleton:") << "\n"
                 << "------------------------------------------------------\n"
                 << configString.str()
                 << "------------------------------------------------------\n"
                 << std::flush;

   // Create Unicode title for bug window
   SC("")
   SC("Do not use characters for the translation that cannot be converted to")
   SC("the ISO 8859-1 character set for this item.")
   SC("Leave the original string in place if in doubt.")
   titleString << S("Bug report") << std::flush;
   SC("")

   // Attention:
   //
   //    const char*  title = titleString.str().c_str()
   //
   // creates 'title' with undefined value because the compiler is allowed to
   // free the temporary string object returned by 'str()' immediately after
   // the assignment!
   // Assigning names to temporary string objects forces them to stay in
   // memory as long as their names go out of scope (this is what we need).
   const std::string&  ts = titleString.str();
   const std::string&  cs = contentString.str();
   const std::string&  config = configString.str();
   const std::string&  subject = subjectString.str();

   // Try to start external e-mail handler
   len = std::strlen(CFG_MAINTAINER);
   p = new char[++len];
   std::strncpy(p, CFG_MAINTAINER, len);
   p[len - (size_t) 1] = 0;  // Not needed, only to silence 'cppcheck'
   // Attention: Overloaded and both prototypes different than in C!
   q = std::strstr(p, "mailto:");
   if(NULL != q)
   {
      maintainer = &q[7];
      rv = enc_percent_decode(maintainer, 1);
      if(0 <= rv)
      {
         rv = ext_handler_email(maintainer, subject.c_str(), config.c_str());
      }
   }
   delete[] p;
   if(rv)
   {
      // Failed => Display information in own window

      // Set "Bug report" window title
#if CFG_USE_XSI && !CFG_NLS_DISABLE
      // Convert window title to encoding required by the window manager (WM)
      // It is assumed that the WM can display either ISO 8859-1 or UTF-8
      // encoded window titles.
      const char*  title;
      title = gui_utf8_iso(ts.c_str());
      if(NULL != title)
      {
         // Display "Bug report" window
         new BugreportWindow(title, cs.c_str());
         // Release memory for window title
         delete[] title;
      }
#else  // CFG_USE_XSI && !CFG_NLS_DISABLE
      // Display "Bug report" window
      new BugreportWindow(ts.c_str(), cs.c_str());
#endif  // CFG_USE_XSI && !CFG_NLS_DISABLE
   }
}


// =============================================================================
// Main Window license information callback

void  MainWindow::license_cb_i(void)
{
   std::ostringstream  titleString;

   // Create Unicode title for license window
   SC("")
   SC("Do not use characters for the translation that cannot be converted to")
   SC("the ISO 8859-1 character set for this item.")
   SC("Leave the original string in place if in doubt.")
   titleString << S("License") << std::flush;
   SC("")

   // Attention:
   //
   //    const char*  title = titleString.str().c_str()
   //
   // creates 'title' with undefined value because the compiler is allowed to
   // free the temporary string object returned by 'str()' immediately after the
   // assignment!
   // Assigning names to temporary string objects forces them to stay in memory
   // as long as their names go out of scope (this is what we need).
   const std::string&  ts = titleString.str();

   // Set "License" window title
#if CFG_USE_XSI && !CFG_NLS_DISABLE
   // Convert window title to the encoding required by the window manager (WM)
   // It is assumed that the WM can display either ISO 8859-1 or UTF-8 encoded
   // window titles.
   const char*  title;
   title = gui_utf8_iso(ts.c_str());
   if(NULL != title)
   {
      // Display "License" window
      new LicenseWindow(title);
      // Release memory for window title
      delete[] title;
   }
#else  // CFG_USE_XSI && !CFG_NLS_DISABLE
   // Display "License" window
   new LicenseWindow(ts.c_str());
#endif  // CFG_USE_XSI && !CFG_NLS_DISABLE
}


// =============================================================================
// Main Window protocol console callback

void  MainWindow::console_cb_i(void)
{
   if(NULL == protocolConsole)
   {
      SC("")
      SC("Do not use characters for the translation that cannot be converted to")
      SC("the ISO 8859-1 character set for this item.")
      SC("Leave the original string in place if in doubt.")
      protocolConsole = new ProtocolConsole(S("Protocol console"));
      SC("")
   }
}


// =============================================================================
// Main Window ROT13 encode/decode callback

void  MainWindow::rot13_cb_i(void)
{
   char*  olddata;
   Fl_Text_Buffer*  newdata = new Fl_Text_Buffer(0, 0);

   if(NULL == currentGroup)
   {
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s", S("No group selected"));
   }
   else if(NULL == currentArticleHE)
   {
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s", S("No article selected"));
   }
   else
   {
      // ROT13 should not be applied twice
      // The clickable references will not work anymore in this case
      if(rot13)
      {
         // Reload article
         aselect_cb_i();
         rot13 = false;
      }
      else
      {
         // Apply ROT13
         olddata = currentArticle->text();
         if(NULL != olddata)
         {
            enc_rot13(olddata);
            newdata->text(olddata);
            articleUpdate(newdata);
         }
         std::free((void*) olddata);
         rot13 = true;
      }
   }
}


// =============================================================================
// Main Window mark single article unread callback
// Now used to toggle (mark read if called again)

void  MainWindow::msau_cb_i(void)
{
   Fl_Tree_Item*  ti;
   core_anum_t  a;

   if(NULL == currentGroup)
   {
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s", S("No group selected"));
   }
   else if(NULL == currentArticleHE)
   {
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s", S("No article selected"));
   }
   else
   {
      ti = articleTree->first_selected_item();
      a = ((core_hierarchy_element*) ti->user_data())->anum;
      // Check whether article was already read
      if(core_check_already_read(&group_list[group_list_index],
         (core_hierarchy_element*) ti->user_data()))
      {
         // Yes => Mark unread
         ti->labelfont(FL_HELVETICA_BOLD);
         core_mark_as_unread(&group_list[group_list_index], a);
         UI_STATUS(S("Marked article unread."));
      }
      else
      {
         // No => Mark read
         ti->labelfont(FL_HELVETICA);
         core_mark_as_read(&group_list[group_list_index], a);
         UI_STATUS(S("Marked article read."));
      }
      articleTree->redraw();
      groupListUpdateEntry(group_list_index);
   }
}


// =============================================================================
// Main Window mark subthread read callback

void  MainWindow::mssar(Fl_Tree_Item*  ti)
{
   core_anum_t  a;
   int  i;

   a = ((core_hierarchy_element*) ti->user_data())->anum;
   // Check whether article was already read
   if(!core_check_already_read(&group_list[group_list_index],
      (core_hierarchy_element*) ti->user_data()))
   {
      // No => Mark read
      ti->labelfont(FL_HELVETICA);
      core_mark_as_read(&group_list[group_list_index], a);
   }
   // Process children recursively
   for(i = 0; ti->children() > i; ++i)
   {
      mssar(ti->child(i));
   }
}


void  MainWindow::mssar_cb_i(void)
{
   Fl_Tree_Item*  ti;

   if(NULL == currentGroup)
   {
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s", S("No group selected"));
   }
   else
   {
      ti = articleTree->first_selected_item();
      if(NULL == currentArticleHE || NULL == ti)
      {
         SC("Do not use non-ASCII for the translation of this item")
         fl_message_title(S("Error"));
         fl_alert("%s", S("No article selected"));
      }
      else
      {
         // Process all article tree nodes in subthread
         // Starting at current article
         mssar(ti);

         // Update article tree
         updateTree();

         // Close subtree (now marked read)
         ti = articleTree->first_selected_item();
         if(NULL != ti)
         {
            articleTree->close(ti);
         }

         // Update group list
         groupListUpdateEntry(group_list_index);
         UI_STATUS(S("Marked all articles in subthread read."));
      }
   }
}


// =============================================================================
// Main Window mark all read callback

void  MainWindow::maar_cb_i(void)
{
   Fl_Tree_Item*  ti;
   core_anum_t  a;

   if(stateMachine(EVENT_A_MAAR))
   {
      if(NULL != currentGroup)
      {
         // Process all article tree nodes (skip root node)
         for(ti = articleTree->first()->next(); ti; ti = articleTree->next(ti))
         {
            ti->labelfont(FL_HELVETICA);
         }
         articleTree->redraw();

         // Mark all articles read
         // Note: CAC was already applied by group selection CB
         if(currentGroup->lwm && currentGroup->hwm >= currentGroup->lwm)
         {
            // Group is not empty
            for(a = currentGroup->hwm; a >= currentGroup->lwm; --a)
            {
               core_mark_as_read(&group_list[group_list_index], a);
            }
        }

         // Update group list
         groupListUpdateEntry(group_list_index);
         UI_STATUS(S("Marked all articles in group read."));
      }

      if(!stateMachine(EVENT_A_MAAR_EXIT))
      {
         PRINT_ERROR("Error in main window state machine");
      }
   }
}


// =============================================================================
// Main Window mark (all articles in) all groups read callback

void  MainWindow::magar_cb_i(void)
{
   Fl_Tree_Item*  ti;
   std::size_t  i;
   core_groupdesc*  g;
   core_anum_t  a;
   int rv;

   if(stateMachine(EVENT_A_MAGAR))
   {
      // Ask for confirmation
      fl_message_title(S("Warning"));
      rv = fl_choice("%s", S("No"),
                     S("Yes"), NULL,
                     S("Really mark all groups read?"));
      if(rv)
      {
         // Process all article tree nodes in current group (skip root node)
         if(NULL != currentGroup)
         {
            for(ti = articleTree->first()->next(); ti;
                ti = articleTree->next(ti))
            {
               ti->labelfont(FL_HELVETICA);
            }
            articleTree->redraw();
         }

         // Loop over all groups
         for(i = 0; group_num > i; ++i)
         {
            // Mark all articles read
            g = &subscribedGroups[i];
            if(g->lwm && g->hwm >= g->lwm)
            {
               // Group is not empty
               for(a = g->hwm; a >= g->lwm; --a)
               {
                  core_mark_as_read(&group_list[i], a);
               }
            }
            // Update corresponding group list entry
            groupListUpdateEntry(i);
         }
         UI_STATUS(S("Marked all articles in all groups read."));
      }

      if(!stateMachine(EVENT_A_MAGAR_EXIT))
      {
         PRINT_ERROR("Error in main window state machine");
      }
   }
}


// =============================================================================
// Main Window article tree callback

void  MainWindow::aselect_cb_i(void)
{
   Fl_Tree_Item*  ti = articleTree->callback_item();

   switch(articleTree->callback_reason())
   {
      case FL_TREE_REASON_OPENED:
      {
         // Check whether article was already read
         if(core_check_already_read(&group_list[group_list_index],
            (core_hierarchy_element*) ti->user_data()))
         {
            // Yes => Show selected article with normal font style
            ti->labelfont(FL_HELVETICA);
         }
         // Scroll tree so that opened item is on top
         scrollTree(UI_SCROLL_TOP, ti);
         break;
      }
      case FL_TREE_REASON_CLOSED:
      {
         // Check whether tree item has unread children
         if(checkTreeBranchForUnread(ti))
         {
            // Yes => Show selected article with bold font style
            ti->labelfont(FL_HELVETICA_BOLD);
         }
         break;
      }
      case FL_TREE_REASON_SELECTED:
      {
#if 1
         // Workaround for unexpected callback with FL_TREE_REASON_SELECTED:
         // Sometimes FLTK create such callbacks even for deactivated entries
         if(NULL == ti->user_data())
         {
            PRINT_ERROR("No data associated with selected article item");
            break;
         }
#endif
         // Check whether operation is allowed at the moment
         if(stateMachine(EVENT_A_PREPARE))
         {
            // Check whether tree item has unread children
            if(ti->is_open() || !checkTreeBranchForUnread(ti))
            {
               // Show selected article with normal font (marked read)
               // if there are no unread children or they are open/visible
               ti->labelfont(FL_HELVETICA);
            }
            // Store former article HE
            lastArticleHE = currentArticleHE;
            // Set current article hierarchy element to corresponding article
            currentArticleHE = (core_hierarchy_element*) ti->user_data();
            // Display corresponding article in content window (use own locking)
            articleSelect(UI_CB_START);
            // Scroll tree, so that selected item is in the middle
            scrollTree(UI_SCROLL_MIDDLE, ti);
            // Store selected item so that it can be restored
            // (this stored state is used later if the operation is not allowed)
            articleTree->store_current(ti);
         }
         else
         {
            // Restore former state if operation is not allowed
            articleTree->select_former();
         }
         break;
      }
      default:
      {
         break;
      }
   }
}


// =============================================================================
// Main window strip 'angle-addr' token of 'From' header field
//
// The data is modified inside the buffer pointed to by 'from'.

void  MainWindow::stripAngleAddress(char*  from)
{
   char*  p;
   char*  q;
   std::size_t  i;
   bool  strip = false;

   // Attention: Overloaded and both prototypes different than in C!
   p = std::strrchr(from, (int) '<');
   // Attention: Overloaded and both prototypes different than in C!
   q = std::strrchr(from, (int) '>');
   if(NULL != p && NULL != q && q > p + 1)
   {
      // 'angle-addr' token present => Check for 'display-name'
      for(i = 0; i < (std::size_t) (p - from); ++i)
      {
         if(' ' != from[i])  { strip = true;  break; }
      }
      if(strip)
      {
         // Strip 'angle-addr' and potential space before it
         if(' ' == *(p - 1))  { --p; }
         *p = 0;
      }
      else
      {
         // Extract 'addr-spec'
         *q = 0;
         std::memmove((void*) from, (void*) (p + 1), (std::size_t) (q - p));
      }
   }
}


// =============================================================================
// Main window reply by e-mail callback

void  MainWindow::sendEmail(void)
{
   const char*  recipient;
   const char*  subject;
   const char*  body = NULL;
   int  rv;
   int  invalid = 0;
   int  warn = 0;
   Fl_Text_Buffer  tb;
   const char*  p;
   char*  q = NULL;
   char*  r = NULL;
   char*  s = NULL;
   char*  t = NULL;
   std::size_t  i = 0;
   char*  sig_delim;
   char*  sig = NULL;
   struct core_article_header*  hdr = NULL;
   char*  from = NULL;
   std::size_t  len;

   if(NULL == currentGroup)
   {
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s", S("No group selected"));
   }
   else if(NULL == currentArticleHE)
   {
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s", S("No article selected"));
   }
   else
   {
      recipient = currentArticleHE->header->reply2;
      if(NULL == recipient)  { recipient = currentArticleHE->header->from; }
      if(NULL == recipient)
      {
         // This should never happen because the core should provide strings for
         // all mandatory header fields.
         PRINT_ERROR("No e-mail address found (bug)");
      }
      else
      {
         // Check address of recipient
         recipient = enc_extract_addr_spec(recipient);
         if(NULL == recipient)  { invalid = 1; }
         else
         {
            // Attention: Overloaded and both prototypes different than in C!
            p = std::strrchr(recipient, (int) '.');
            if(p)
            {
               // Check for reserved top level domains according to RFC 2606
               if(!std::strcmp(p, ".test"))  { invalid = 1; }
               if(!std::strcmp(p, ".example"))  { invalid = 1; }
               if(!std::strcmp(p, ".invalid"))  { invalid = 1; }
               if(!std::strcmp(p, ".localhost"))  { invalid = 1; }
            }
            if(!invalid)
            {
               // Cite article
               hdr = currentArticleHE->header;
               t = currentArticle->text();
               if(NULL != t)
               {
                  // Strip header
                  while('_' != t[i++]);
                  while('|' != t[i++]);
                  while('|' != t[i++]);
                  q = &t[++i];
                  // Strip signature
                  r = q;
                  while(1)
                  {
                     // Attention:
                     // Overloaded and both prototypes different than in C!
                     sig_delim = std::strstr(r, "\n-- \n");
                     if(NULL == sig_delim)  { break; }
                     else  { sig = r = &sig_delim[1]; }
                  }
                  if(NULL != sig)  { sig[0] = 0; }
                  // Extract authors 'display-name' and strip 'angle-addr'
                  len = std::strlen(hdr->from);
                  from = new char[++len];
                  std::strcpy(from, hdr->from);
                  stripAngleAddress(from);
                  tb.insert(0, q);
                  gui_cite_content(&tb, from, hdr->groups);
                  body = tb.text();
                  delete[] from;
                  // Do not convert body to canonical form here!
               }
               // Create subject
               subject = currentArticleHE->header->subject;
               s = new char[std::strlen(subject) + (std::size_t) 5];
               std::strcpy(s, "Re: ");
               std::strcat(s, gui_check_re_prefix(subject));
               // Attention: Overloaded and both prototypes different than in C!
               q = std::strstr(s, "(was:");
               if(q && q != s)
               {
                  // Attention:
                  // Overloaded and both prototypes different than in C!
                  if(std::strchr(q, (int) ')'))
                  {
                     *q = 0;
                     if(' ' == *(--q))  { *q = 0; }
                  }
               }
               subject = s;
               // Open warning popup if subject or body contains apostroph chars
               if(NULL != subject)
               {
                  // Attention:
                  // Overloaded and both prototypes different than in C!
                  if(std::strchr(subject, 0x27))  { warn = 1; }
               }
               if(!warn && NULL != body)
               {
                  // Attention:
                  // Overloaded and both prototypes different than in C!
                  if(std::strchr(body, 0x27))  { warn = 1; }
               }
               if(warn)
               {
                  SC("Do not use non-ASCII for the translation of this item")
                  fl_message_title(S("Note"));
                  fl_message("%s",
                     S("APOSTROPHE converted to\nRIGHT SINGLE QUOTATION MARK"));
               }
               // Start external e-mail handler
               rv = ext_handler_email(recipient, subject, body);
               if(rv)
               {
                  SC("Do not use non-ASCII for the translation of this item")
                  fl_message_title(S("Error"));
                  fl_alert("%s", S("Starting e-mail client failed"));
               }
               delete[] s;
               std::free((void*) body);
               std::free((void*) t);
            }
            enc_free((void*) recipient);
         }
         if(invalid)
         {
            SC("Do not use non-ASCII for the translation of this item")
            fl_message_title(S("Error"));
            fl_alert("%s", S("Invalid e-mail address"));
         }
      }
   }
}


// =============================================================================
// Main Window scroll down article content and skip to next article
// (must be locked)

void  MainWindow::ascrolldown_cb(bool  scroll)
{
   int  r;        // Number of rows
   int  f = -1;   // First row
   int  l = -1;   // Last row
   int  p = 0;    // Position in text buffer
   int  riv = 0;  // Number of rows currently in view
   int  scrollto;
   int  x, y;
   int  i;
   bool  eoa = true;
   Fl_Tree_Item*  fi;
   Fl_Tree_Item*  ti;
   Fl_Tree_Item*  oi;
   bool  abort = false;
   bool  wrap = false;
   bool  found = false;

   // Check whether operation is allowed at the moment
   if(stateMachine(EVENT_SCROLL_NEXT))
   {
      // Scroll down if requested
      if(scroll)
      {
         // Calculate number of rows
         if(NULL != currentArticle && text->buffer() == currentArticle)
         {
            r = text->count_lines(0, text->buffer()->length(), true);
            if(0 < r)
            {
               for(i = 0; i < r; ++i)
               {
                  if(text->position_to_xy(p, &x, &y))
                  {
                     if(0 > f)  { l = f = i; }
                     else  { l = i; }
                  }
                  p = text->skip_lines(p, 1, true);
               }
               if(0 > f)  { f = 0;  l = 0; }
               currentLine = f;
               riv = l - f + 1;
            }
#if 0
            // For debugging
            std::printf("-----------------------------\n");
            std::printf("Rows         : %d\n", r);
            std::printf("Current row  : %d\n", currentLine);
            std::printf("First in view: %d\n", f);
            std::printf("Last in view : %d\n", l);
            std::printf("Rows in view : %d\n", riv);
#endif

            // Scroll down so that second last row will become first in view
            if(r && riv)
            {
               if(r - 1 != l)
               {
                  scrollto = currentLine + riv - 2;
                  if(scrollto < r - 1)
                  {
                     // std::printf("scrollto: %d\n", scrollto);
                     text->scroll(scrollto + 1, 0);
                     eoa = false;
                  }
               }
            }
         }
      }

      // Skip to next unread article if at the end of current article
      if(eoa)
      {
         // Start with selected article (or first article if there is none)
         fi = articleTree->first_selected_item();
         if(NULL == fi)
         {
            fi = articleTree->first();
            if(fi == articleTree->root())  { fi = articleTree->next(fi); }
         }
         // Check for dummy article
         if(NULL != fi && NULL == fi->user_data())  { fi = NULL; }
         ti = fi;
         while(NULL != ti)
         {
            if(!core_check_already_read(&group_list[group_list_index],
               (core_hierarchy_element*) ti->user_data()))
            {
               // Open branch with next article
               oi = ti;
               while(articleTree->root() != oi)
               {
                  if(!articleTree->open(oi))  { articleTree->open(oi, 1); }
                  for(i = 0; i < articleTree->root()->children(); ++i)
                  {
                     if(articleTree->root()->child(i) == oi)
                     {
                        abort = true;
                        break;
                     }
                  }
                  if(abort)  { break; }
                  oi = articleTree->prev(oi);
               }
               // Select next article
               articleTree->deselect(fi, 0);
               articleTree->set_item_focus(ti);
               articleTree->select(ti, 1);
               found = true;
               break;
            }
            ti = articleTree->next(ti);
            if(NULL == ti && !wrap)
            {
               // Wrap to beginning once if no article was found yet
               wrap = true;
               ti = articleTree->first();
               if(ti == articleTree->root())  { ti = articleTree->next(ti); }
            }
         }
      }
      if(!stateMachine(EVENT_SCROLL_NEXT_EXIT))
      {
         PRINT_ERROR("Error in main window state machine");
      }

      // Sip to next group with unread articles (if no article was found)
      if (eoa && !found)
      {
         if (config[CONF_UNREAD_IN_NEXT_GROUP].val.i)  { nug_cb_i(); }
      }
   }
}


// =============================================================================
// Main Window server configuration update
// (must be locked)

void  MainWindow::updateServer(int  action)
{
   SC("")
   SC("Do not use characters for the translation that cannot be converted to")
   SC("the ISO 8859-1 character set for this item.")
   SC("Leave the original string in place if in doubt.")
   const char*  titleString = S("Server configuration");
   SC("")
   int  rv = -1;
   int  cancel = 0;
   ServerCfgWindow*  scw = NULL;
   ServerConfig*  sc = NULL;
   const char*  user = NULL;
   const char*  pass = NULL;

   // Start core to do the work
   if(UI_CB_START == action)
   {
      // Check whether operation is allowed at the moment
      if(!stateMachine(EVENT_SERVER))  { rv = 1; }
      else
      {
         // Display warning message
         data.data = NULL;
         SC("Do not use non-ASCII for the translation of this item")
         fl_message_title(S("Note"));
         SC("Line breaks are inserted with \n")
         fl_message("%s",
           S("Warning:\nGroup states are lost if the server is changed."));

         // Create object with current server configuration
         sc = new ServerConfig;
         sc->serverReplace(config[CONF_SERVER].val.s);
         sc->serviceReplace(config[CONF_SERVICE].val.s);
         sc->enc = config[CONF_ENC].val.i;
         sc->auth = config[CONF_AUTH].val.i;

         // Display "Server configuration" window
#if CFG_USE_XSI && !CFG_NLS_DISABLE
         // Convert window title to the encoding required by the window manager
         // (WM). It is assumed that the WM can display either ISO 8859-1 or
         // UTF-8 encoded window titles.
         const char*  title;
         title = gui_utf8_iso(titleString);
         if(NULL != title)
         {
            scw = new ServerCfgWindow(sc, title);
            delete[] title;
         }
#else  // CFG_USE_XSI && !CFG_NLS_DISABLE
         scw = new ServerCfgWindow(sc, titleString);
#endif  // CFG_USE_XSI && !CFG_NLS_DISABLE
         if(NULL == scw)
         {
            // This should never happen
            PRINT_ERROR("Fatal error while creating server config window");
            exitRequest = 1;
         }
         else
         {
            if(0 < scw->process())
            {
               // Start operation
               UI_STATUS(S("Update server configuration ..."));
               UI_BUSY();
               core_mutex_lock();
               data.data = (void*) sc;
               core_mutex_unlock();
               // Disconnect so that changes take effect
               core_disconnect();
               // Export current groups (this creates groupfile if not present)
               rv = core_export_group_states(group_num, group_list);
               if(!rv)
               {
                  // Check whether server name has changed
                  if(std::strcmp(config[CONF_SERVER].val.s, sc->server))
                  {
                     // Reset states in groupfile and delete header database
                     rv = core_reset_group_states(UI_CB_COOKIE_SERVER);
                  }
               }
            }
            else  { cancel = 1; }
            delete scw;
         }
      }
   }
   else
   {
      // Get result
      core_mutex_lock();
      rv = data.result;
      core_mutex_unlock();
   }

   // Check return value (positive value means "in progress")
   if(0 >= rv)
   {
      if(0 > rv)
      {
         if (!cancel)
         {
            // Operation failed
            UI_STATUS(S("Updating server configuration failed."));
            UI_READY();
         }
      }
      else
      {
         // Restore pointer to server string
         core_mutex_lock();
         sc = (ServerConfig*) data.data;
         core_mutex_unlock();
         // Replace server name in configuration
         // (This must be done after the header database was reset and ready for
         // the new server)
         conf_string_replace(&config[CONF_SERVER], sc->server);
         conf_string_replace(&config[CONF_SERVICE], sc->service);
         config[CONF_ENC].val.i = sc->enc;
         config[CONF_AUTH].val.i = sc->auth;
         if(UI_AUTH_USER == sc->auth)
         {
            // Ask for account name
            user = fl_input("%s", config[CONF_USER].val.s, "Login:");
            if(NULL != user)  { conf_string_replace(&config[CONF_USER], user); }
            // Ask for password
            SC("Do not use non-ASCII for the translation of this item")
            fl_message_title(S("Note"));
            rv = fl_choice("%s", S("No"),
                           S("Yes"), NULL,
                           S("Store the password from the subsequent request?"));
            if(rv)  { conf_ephemeral_passwd = 0; }
            else  { conf_ephemeral_passwd = 1; }
            do
            {
               pass = fl_password("%s", config[CONF_PASS].val.s, "Password:");
               if(NULL != pass)
               {
                  conf_string_replace(&config[CONF_PASS], pass);
                  if(!config[CONF_PASS].val.s[0])
                  {
                     SC("Do not use non-ASCII for the translation of this item")
                     fl_message_title(S("Warning"));
                     fl_alert("%s", S("Empty password is not supported"));
                  }
               }
            }  while(NULL == pass || !config[CONF_PASS].val.s[0]);
         }
         // Reset CRL update interval back to epoch (force new CRLs)
         conf_string_replace(&config[CONF_CRL_UPD_TS], "1970-01-01T00:00:00Z");
         // Check for empty group list
         if(!group_num)
         {
            // Ask for password
            SC("Do not use non-ASCII for the translation of this item")
            fl_message_title(S("Note"));
            rv = fl_choice("%s", S("No"),
                           S("Yes"), NULL,
                           S("Ask server for group subscription proposals?"));
            if(rv)
            {
               // Ask server for subscription proposals
               groupListGetProposal(UI_CB_START);
            }
            else
            {
               UI_STATUS(S("No groups subscribed yet."));
               UI_READY();
            }
         }
         else
         {
            // Recreate group list
            core_destroy_subscribed_group_states(&group_num, &group_list);
            groupListRefresh(UI_CB_START);
         }
      }
      if(NULL != sc)  { delete sc; }
      if(!stateMachine(EVENT_SERVER_EXIT))
      {
         PRINT_ERROR("Error in main window state machine");
      }
   }
}


// =============================================================================
// Main window group subscription
// (must be locked)

void  MainWindow::groupSubscribe(int  action)
{
   int  rv = -1;
   std::size_t  labelcount;
   core_grouplabel*  labels;
   std::size_t  i, ii, j;
   char*  name;
   const char*  label;
   int  free_label;
   core_anum_t  num;
   int  fake_empty_response = 0;

   // Start core to do the work
   if(UI_CB_START == action)
   {
      if(!stateMachine(EVENT_SUBSCRIBE))  { rv = 1; }
      else
      {
         // Start operation
         UI_STATUS(S("Receiving group list ..."));
         UI_BUSY();
         rv = core_get_group_list(UI_CB_COOKIE_GROUPLIST);
         // Note: This will generate callback with action UI_CB_CONTINUE
      }
   }
   else
   {
      // Get result
      core_mutex_lock();
      rv = data.result;
      core_mutex_unlock();
   }

   if(!rv && UI_CB_CONTINUE == action)
   {
      core_mutex_lock();
      groupcount = data.size;
      grouplist = (core_groupdesc*) data.data;
      core_mutex_unlock();
      // Start next operation
      UI_STATUS(S("Receiving group labels ..."));
      UI_BUSY();
      rv = core_get_group_labels(UI_CB_COOKIE_GROUPLABELS);
      // Note: This will generate callback with action UI_CB_FINISH
      if(0 > rv)  { fake_empty_response = 1; }
   }

   if(0 > rv && UI_CB_FINISH == action)  { fake_empty_response = 1; }

   if (fake_empty_response)
   {
      // Fake valid empty response
      core_mutex_lock();
      data.size = 0;
      data.data = NULL;
      core_mutex_unlock();
      rv = 0;
   }

   // Check return value (positive value means "in progress")
   if(0 >= rv)
   {
      if(0 > rv)
      {
         // Operation failed
         UI_STATUS(S("Downloading group information failed."));
         UI_READY();
      }
      else
      {
         // Operation finished
         core_mutex_lock();
         labelcount = data.size;
         labels = (core_grouplabel*) data.data;
         core_mutex_unlock();
         UI_STATUS(S("Group information received."));
         // Create subscribe window
         SC("")
         SC("Do not use characters for the translation that cannot be")
         SC("converted to the ISO 8859-1 character set for this item.")
         SC("Leave the original string in place if in doubt.")
         subscribeWindow = new SubscribeWindow(S("Subscribe"),
                                               grouplist, labels);
         SC("")
         // Add groups
         UI_STATUS(S("Process group information ..."));
         subscribeWindow->hide();
         Fl::wait();
         for(i = 0; i < groupcount; ++i)
         {
            free_label = 0;
            // Do not remove this check until you know what you are doing!
            if(CORE_GROUP_FLAG_ASCII & grouplist[i].flags)
            {
               // Search for label
               name = grouplist[i].name;
               num = grouplist[i].eac;
               label = NULL;
               for(j = 0; j < labelcount; ++j)
               {
                  if(!strcmp(labels[j].name, name))
                  {
                     // Group desciption label found
                     // Verify UTF-8 encoding of label
                     if(0 == enc_uc_check_utf8(labels[j].label))
                     {
                        // Valid
                        label = labels[j].label;
                     }
                     else
                     {
                        // Not valid => Repair UTF-8 encoding
                        PRINT_ERROR("Invalid encoding of group description");
                        label = enc_uc_repair_utf8(labels[j].label);
                        if(NULL != label)  { free_label = 1; }
                     }
                     break;
                  }
               }
               // Replace dots with slashes to create tree hierarchy
               ii = 0;
               do  { if('.' == name[ii])  { name[ii] = '/'; } }
               while(name[ii++]);
               subscribeWindow->add(name, num, label);
               if(free_label)  { core_free((void*) label); }
            }
         }
         // Collapse all branches of tree (and append labels, if present)
         // Must be the last operation after all groups are added
         subscribeWindow->collapseAll();
         subscribeWindow->show();
         UI_STATUS(S("Group information processed."));
         UI_READY();
         // The destructor of 'subscribeWindow' will release the memory for
         // 'grouplist' and 'labels'. Because this window is modal, no other
         // operations are possible in the meanwhile.
      }
      if(!stateMachine(EVENT_SUBSCRIBE_EXIT))
      {
         PRINT_ERROR("Error in main window state machine");
      }
   }
}


// =============================================================================
// Main window calculate number of unread articles in group

core_anum_t  MainWindow::groupGetUnreadNo(core_anum_t  lwm, core_anum_t  hwm,
                                          struct core_range*  info)
{
   core_anum_t  res = 0;
   core_anum_t  i;

   if(lwm && hwm && hwm >= lwm)
   {
      res = hwm - lwm + (core_anum_t) 1;
   }

   if(res)
   {
      while(NULL != info)
      {
         for(i = info->last; i >= info->first; --i)
         {
            if(i >= lwm && i <= hwm)  { if(res)  { --res; } }
         }
         info = info->next;
      }
   }

   return(res);
}


// =============================================================================
// Main window update entry i in group list

void  MainWindow::groupListUpdateEntry(std::size_t  i)
{
   std::ostringstream  groupString;
   core_anum_t  eac;
   core_anum_t  ur;
   const char*  fcs;

   // Show not more than 500 groups
   if((std::size_t) 500 >= i)
   {
      // Clamp article count to process
      groupCAC(&subscribedGroups[i]);

      // Calculate number of unread articles
      eac = subscribedGroups[i].eac;
      ur = groupGetUnreadNo(subscribedGroups[i].lwm, subscribedGroups[i].hwm,
                            group_list[i].info);
      // Create entry for group list widget
      if(ur)  { fcs = "@b"; }  else  { fcs = "@."; }
      groupString << fcs << group_list[i].name
                  << " (" << ur << " / " << eac << ")" << std::flush;

      // Attention:
      //
      //    const char*  s = groupString.str().c_str()
      //
      // creates 's' with undefined value because the compiler is allowed to
      // free the temporary string object returned by 'str()' immediately after
      // the assignment!
      // Assigning names to temporary string objects forces them to stay in
      // memory as long as their names go out of scope (this is what we need).
      const std::string&  gs = groupString.str();

      // Note: The index in the group list widget starts with 1
      if(++i > (std::size_t) groupList->size())
      {
         // Create new entry
         groupList->add(gs.c_str(), NULL);
      }
      else
      {
         // Replace label of existing entry
         groupList->text((int) i, gs.c_str());
      }
   }
}


// =============================================================================
// Merge current group states into groupfile

int  MainWindow::groupStateMerge(void)
{
   int  res;

   // Skip this if group count is zero (or the groupfile will be cleared)
   if(group_num)
   {
      res = core_export_group_states(group_num, group_list);
      if(res)
      {
         SC("Do not use non-ASCII for the translation of this item")
         fl_message_title(S("Error"));
         fl_alert("%s", S("Exporting group states failed"));
      }
   }
   // Re-import current group states from groupfile
   res = core_update_subscribed_group_states(&group_num, &group_list,
                                             &group_list_index);

   return(res);
}


// =============================================================================
// Main window ask server for proposed groups to subscribe
// (must be locked)

void  MainWindow::groupListGetProposal(int  action)
{
   int  rv = -1;
   char*  grouplist_raw;

   // Start core to do the work
   if(UI_CB_START == action)
   {
      // Group list not empty
      if(!stateMachine(EVENT_GL_PROPOSAL))  { rv = 1; }
      else
      {
         // Get group states from server
         UI_STATUS(S("Get group subscription proposals ..."));
         UI_BUSY();
         rv = core_get_subscription_proposals(UI_CB_COOKIE_GROUPPROPOSAL);
      }
   }
   else
   {
      // Get result
      core_mutex_lock();
      rv = data.result;
      core_mutex_unlock();
   }

   // Check return value (positive value means "in progress")
   if(0 >= rv)
   {
      if(0 > rv)
      {
         // Operation failed
         UI_STATUS(S("Receiving proposed groups failed."));
         UI_READY();
      }
      else
      {
         core_mutex_lock();
         grouplist_raw = (char*) data.data;
         core_mutex_unlock();
         // Populate group list
         rv = gui_populate_group_list(grouplist_raw);
         if(rv)
         {
            PRINT_ERROR("Updating list of subscribed groups failed");
         }
         core_free(grouplist_raw);
         // Refresh group list
         groupListRefresh(UI_CB_START);
      }
      if(!stateMachine(EVENT_GL_PROPOSAL_EXIT))
      {
         PRINT_ERROR("Error in main window state machine");
      }
   }
}


// =============================================================================
// Main window refresh group list with subscribed groups
// (must be locked)

void  MainWindow::groupListRefresh(int  action)
{
   int  rv = -1;
   std::size_t  i;
   int  sg = 0;  // Selected group
   int  g;

   // Start core to do the work
   if(UI_CB_START == action)
   {
      // Group list not empty
      if(!stateMachine(EVENT_GL_REFRESH))  { rv = 1; }
      else
      {
         rv = groupStateMerge();
         if(!rv)
         {
            // Get group states from server
            UI_STATUS(S("Refreshing subscribed groups ..."));
            UI_BUSY();
            for(g = 1; groupList->size() >= g; ++g)
            {
               if(groupList->selected(g))  { sg = g;  break; }
            }
            groupRefresh_cb_state = sg;
            rv = core_get_subscribed_group_info(&group_num, &group_list,
                                                UI_CB_COOKIE_GROUPINFO1);
            // Note: This will generate callback with action UI_CB_CONTINUE
         }
      }
   }
   else
   {
      // Get result
      core_mutex_lock();
      rv = data.result;
      core_mutex_unlock();
   }

   if(!rv && UI_CB_CONTINUE == action)
   {
      // Continue operation
      if(NULL != subscribedGroups)
      {
         core_destroy_subscribed_group_info(&subscribedGroups);
      }
      core_mutex_lock();
      subscribedGroups = (core_groupdesc*) data.data;
      core_mutex_unlock();
      // Update group list widget
      groupList->clear();
      for(i = 0; i < group_num; ++i)  { groupListUpdateEntry(i); }
      // Restore current group of server
      if(group_num && NULL != currentGroup)
      {
         rv = core_set_group(group_list[group_list_index].name,
                             UI_CB_COOKIE_GROUPINFO2);
         // Note: This will generate callback with action UI_CB_FINISH
      }
   }

   if(!rv && UI_CB_FINISH == action)
   {
      // Update current group descriptor
      core_free(currentGroup);
      core_mutex_lock();
      currentGroup = (core_groupdesc*) data.data;
      core_mutex_unlock();
   }

   // Check return value (positive value means "in progress")
   if(0 >= rv)
   {
      if(0 > rv)
      {
         // Operation failed
         clearTree();
         core_free(currentGroup);
         currentGroup = NULL;
         UI_STATUS(S("Refreshing subscribed groups failed."));
      }
      else
      {
         groupList->deselect();
         if(!group_num || unsub)
         {
            // Delete article tree and current article
            clearTree();
            core_free(currentGroup);
            currentGroup = NULL;
         }
         else
         {
            if (0 == groupRefresh_cb_state)
            {
               if (0 != groupList->size())
               {
                  groupList->select(1, 0);
                  // FLTK 1.4.0 does not correctly draw the focus box
                  groupList->redraw();
               }
            }
            else
            {
               // Reset group list widget
               groupList->select(groupRefresh_cb_state);
               groupList->middleline(groupRefresh_cb_state);
            }
         }
         // Operation finished
         UI_STATUS(S("Subscribed groups refreshed."));
      }
      unsub = false;
      UI_READY();
      if(!stateMachine(EVENT_GL_REFRESH_EXIT))
      {
         PRINT_ERROR("Error in main window state machine");
      }
   }
}


// =============================================================================
// Main window group selection
// (must be locked)

void  MainWindow::groupSelect(int  action, int  index)
{
   int  rv = -1;

   if(UI_CB_START == action)
   {
      if(!index || !stateMachine(EVENT_G_SELECT))
      {
         if(NULL != currentGroup)
         {
            // Reset group list window if operation is not allowed
            if((std::size_t) INT_MAX > group_list_index)
            {
               groupList->deselect();
               groupList->select((int) group_list_index + 1);
            }
         }
         rv = 1;
      }
      else
      {
         // Start core to do the work
         if(0 < index)
         {
            group_new = index;
            if(group_old)
            {
               group_old = 0;
               if((std::size_t) INT_MAX > group_list_index)
               {
                  group_old = (int) group_list_index + 1;
               }
            }
            group_list_index = (std::size_t) index;
            if(--group_list_index < group_num)
            {
               UI_STATUS(S("Set new current group ..."));
               UI_BUSY();
               groupSelect_cb_state = groupList->value();
               rv = core_set_group(group_list[group_list_index].name,
                                   UI_CB_COOKIE_GROUP);
            }
         }
      }
   }
   else
   {
      // Get result
      core_mutex_lock();
      rv = data.result;
      core_mutex_unlock();
   }

   // Check return value (positive value means "in progress")
   if(0 >= rv)
   {
      UI_READY();
      if(0 > rv)
      {
         clearTree();
         UI_STATUS(S("Setting new current group failed."));
         // After error, the previous group is still selected
         if(0 >= group_old)
         {
            // No previous group
            groupList->select(group_new, 0);
            groupList->make_visible(group_new);
         }
         else
         {
            group_list_index = group_old - 1;
            groupList->deselect();
            groupList->select(group_old);
            groupList->make_visible(group_old);
         }
         if(stateMachine(EVENT_AT_REFRESH))
         {
            if(!stateMachine(EVENT_AT_REFRESH_EXIT))
            {
               PRINT_ERROR("Error in main window state machine");
            }
         }
      }
      else
      {
         // Success, update current group
         group_old = -1;
         core_free(currentGroup);
         core_mutex_lock();
         currentGroup = (core_groupdesc*) data.data;
         core_mutex_unlock();
         groupList->make_visible(group_new);
         // Clamp article count to process
         groupCAC(currentGroup);
         UI_STATUS(S("New current group set."));

         // Update article tree
         lastArticleHE = NULL;
         currentArticleHE = NULL;
         updateArticleTree(UI_CB_START);
      }
      if(!stateMachine(EVENT_G_SELECT_EXIT))
      {
         PRINT_ERROR("Error in main window state machine");
      }
   }
}


// =============================================================================
// Main window clamp article count to configured value

void  MainWindow::groupCAC(core_groupdesc*  g)
{
   int  i = config[CONF_CAC].val.i;
   core_anum_t  diff;

   if(NULL != g && 0 < i--)
   {
      // Ensure that group is not empty
      if(g->hwm >= g->lwm && g->eac)
      {
         // Calculate new watermark difference
         diff = (core_anum_t) i;
         // Check whether clamping is required
         if(g->hwm - g->lwm > diff)  { g->lwm = g->hwm - diff; }
      }
   }
}


// =============================================================================
// Main window clear article tree (and current article)

void  MainWindow::clearTree(void)
{
   Fl_Text_Buffer*  tb;
   Fl_Tree_Item*  ti;

   // Delete article tree
   articleTree->item_labelfont(FL_HELVETICA);
   articleTree->clear();
   ti = articleTree->add(S("No articles"));
   if(NULL != ti)  { ti->deactivate(); }
   articleTree->redraw();
   // Hierarchy stays in memory until next group is selected
   // Delete last article
   lastArticleHE = NULL;
   // Delete current article
   currentArticleHE = NULL;
   // Preserve greeting message on startup
   if(startup)  { startup = false; }
   else
   {
      tb = new Fl_Text_Buffer(0, 0);
      articleUpdate(tb);
   }
}


// =============================================================================
// Main Window scroll article tree
//
// The original Fl_Tree widget of 1.3.0 requires the following algorithm for
// scrolling to work correctly:
// - Set vertical position to zero
// - Redraw widget to update internal state
// - Wait until redraw is complete
// - Scroll to target position
//
// Since 1.3.3 the new Fl_Tree widget provides a 'calc_tree()' method.

void  MainWindow::scrollTree(ui_scroll  position, Fl_Tree_Item*  ti)
{
   bool  old_tree_widget = true;

   // Prepare internal state of widget
#ifdef FL_ABI_VERSION
#  if 10303 <= FL_ABI_VERSION
   // Requires FLTK 1.3.3
   articleTree->hposition(0);
   articleTree->calc_tree();
   articleTree->hposition(0);
   old_tree_widget = false;
#  endif  // 10303 <= FL_ABI_VERSION
#endif  // FL_ABI_VERSION
   if(old_tree_widget)
   {
      // Move vertical scrollbar to the top first
      articleTree->vposition(0);
      // Redraw tree so that new internal state is calculated for scrolling
      articleTree->redraw();
      articleTree->not_drawn();
      // Wait until redraw is finished
      while(!articleTree->drawn())  { Fl::wait(0.10); }
      // Wait until redraw is complete
   }

   // Scroll horizontally
#ifdef FL_ABI_VERSION
#  if 10303 <= FL_ABI_VERSION
   // Requires FLTK 1.3.3
   gui_set_default_font();
   {
      int xmax = ti->draw_item_content(0);
      int scroll = xmax - (articleTree->x() + articleTree->w());

      // Check whether vertical scrollbar is visible
      if(articleTree->is_vscroll_visible())
      {
         // Yes => Add width of vertical scrollbar
         int w_sb = articleTree->scrollbar_size();
         if (0 == w_sb)  { w_sb = Fl::scrollbar_size(); }
         scroll += w_sb;
      }
      if (0 < scroll)
      {
         // Check whether left side will be scrolled out of view
         int left = ti->x() - articleTree->x();
         if (left < scroll)
         {
            // Yes => Scroll to the left side
            scroll = left;
         }
         articleTree->hposition(scroll);
      }
   }
#  endif  // 10303 <= FL_ABI_VERSION
#endif  // FL_ABI_VERSION

   // Scroll vertically
   switch(position)
   {
      case UI_SCROLL_TOP:
      {
         articleTree->show_item_top(ti);
         break;
      }
      case UI_SCROLL_MIDDLE:
      {
         articleTree->show_item_middle(ti);
         break;
      }
      case UI_SCROLL_BOTTOM:
      {
         articleTree->show_item_bottom(ti);
         break;
      }
      case UI_SCROLL_NONE:
      default:
      {
         // Ignore ti pointer
         break;
      }
   }

   articleTree->redraw();
}


// =============================================================================
// Main Window check whether tree branch below item contains unread articles
// This method must be reentrant.

bool  MainWindow::checkTreeBranchForUnread(Fl_Tree_Item*  ti)
{
   bool  res = false;
   int  c;
   int  i;

   if(ti->children())
   {
      c = ti->children();
      for(i = 0; i < c; ++i)
      {
         // Check this child
         if(FL_HELVETICA_BOLD == ti->child(i)->labelfont())  { res = true; }
         else
         {
            // Recursively check children of this child
            if(checkTreeBranchForUnread(ti->child(i)))  { res = true; }
         }
         if(true == res)  { break; }
      }
   }

   return(res);
}


// =============================================================================
// Main Window check whether tree branch below 'ti' contains item 'sti'
// This method must be reentrant.

bool  MainWindow::checkTreeBranchForItem(Fl_Tree_Item*  ti, Fl_Tree_Item*  sti)
{
   bool  res = false;
   int  c;
   int  i;

   if(ti->children())
   {
      c = ti->children();
      for(i = 0; i < c; ++i)
      {
         // Check this child
         if(ti->child(i) == sti)  { res = true; }
         else
         {
            // Recursively check children of this child
            if(checkTreeBranchForItem(ti->child(i), sti))  { res = true; }
         }
         if(true == res)  { break; }
      }
   }

   return(res);
}


// =============================================================================
// Main window add children to article tree node
//
// \param[in] cti  Pointer to node of tree widget
// \param[in] che  Pointer to node of core article hierarchy
//
// Call this method with \e cti and \e che pointing to the root node of the
// corresponding hierarchy. To add the child nodes, this method calls itself
// recursively and must therefore be reentrant.
//
// This method first adds every node with bold font (marked unread).
// Then the corresponding article is checked. If it's already read, the font is
// changed to normal style (marked read).
//
// \note
// This method is called for every article until CAC limit. It should be fast.
//
// \return
// - The corresponding tree item if the current article was found
// - \c NULL otherwise

Fl_Tree_Item*  MainWindow::addTreeNodes(Fl_Tree_Item*  cti,
                                        core_hierarchy_element*  che)
{
   static const char  t[] = "   |   ";  // Field separator
   std::size_t  t_len = sizeof(t);
   const char*  s;  // Subject
   std::size_t  s_len;
   char s_tmp;  // Temporary buffer used to convert HTAB to SP
   const char*  f;  // From
   std::size_t  f_len;
   char*  f_tmp;  // Pointer to temporary buffer used for <angle-addr>
   core_time_t  d_raw;
   char  d[20];  // Date (Format: "YYYY-MM-DD HH:MM:SS")
   std::size_t  d_len;
#if USE_LINE_COUNT
   unsigned long int  l_raw;
   char  l[11];  // Lines
#endif  // USE_LINE_COUNT
#if USE_ARTICLE_NUMBER
   char  a[17];  // Article number
#endif  // USE_ARTICLE_NUMBER
   std::size_t  l_len = 0;
   std::size_t  a_len = 0;
   Fl_Tree_Item*  res = NULL;
   Fl_Tree_Item*  tmp = NULL;
   Fl_Tree_Item*  nti = NULL;
   char*  ss;
   std::size_t  ssi;
   std::size_t  i;
   std::size_t  ii;
   int  rv;
   int  score;
   // For merging orphaned thread branches
   bool  child_of_root_node = false;
   Fl_Tree_Item*  rti;
   int  iii;
   struct core_article_header*  h;
   struct core_article_header*  h2;
   // For unthreaded view
   core_time_t  rd;
   core_anum_t  n;
   core_anum_t  rn;
   bool  match;
   // Kill threshold
   int kill_threshold = conf_integer_check(CONF_SCORE_KILL_TH, -49999, 0);

   // Explicitly set foreground color for tree items to FLTK default
   // (default is not the same as for the other widgets)
   articleTree->item_labelfgcolor(FL_FOREGROUND_COLOR);

   // Set default font to bold (mark unread)
   articleTree->item_labelfont(FL_HELVETICA_BOLD);

   // Add children of current hierarchy element
   if(articleTree->root() == cti)  { child_of_root_node = true; }
   for(i = 0; i < che->children; ++i)
   {
      n = che->child[i]->anum;
      // Create description line in format "Subject | From | Date[ | Lines]"
      // Prepare data and calculate length
      s = che->child[i]->header->subject;
      s_len = std::strlen(s);
      f = che->child[i]->header->from;
      f_len = t_len + std::strlen(f);
      d_raw = che->child[i]->header->date;
      rv = enc_convert_posix_to_iso8601(d, d_raw);
      if(rv)  { d_len = 0; }  else  { d_len = t_len + (std::size_t) 20; }
#if USE_LINE_COUNT
      l_raw = che->child[i]->header->lines;
      enc_convert_lines_to_string(l, l_raw);
      l_len = t_len + (std::size_t) 11;
#else  // USE_LINE_COUNT
      l_len = 0;
#endif  // USE_LINE_COUNT
#if USE_ARTICLE_NUMBER
      rv = enc_convert_anum_to_ascii(a, &a_len, che->child[i]->anum);
      if(rv)
      {
         a[0] = 'E';
         a[1] = 'r';
         a[2] = 'r';
         a[3] = 'o';
         a[4] = 'r';
         a[5] = 0;
         a_len = 0;
      }
      else  { a_len += t_len; }
#else  // USE_ARTICLE_NUMBER
      a_len = 0;
#endif  // USE_ARTICLE_NUMBER
      // Allocate buffer for description line
      ss = new char[s_len + f_len + d_len + l_len + a_len + (std::size_t) 1];
      ssi = 0;  // Current index in 'ss'
      // -----------------------------------------------------------------------
      // Subject
      ii = 0;  while(s[ii])
      {
         s_tmp = s[ii++];
         // The NNTP command OVER cannot transport HTAB characters (converts
         // them to SP according to RFC 3977). We always do this here to get
         // consistent results for cases where OVER is not used or the HTAB was
         // transported inside a MIME encoded-word.
         if (0x09 == (int) s_tmp)  { ss[ssi++] = ' '; }
         else  { ss[ssi++] = s_tmp; }
      }
      // -----------------------------------------------------------------------
      // From
      ii = 0;  while(t[ii])  { ss[ssi++] = t[ii++]; }
      // Strip 'angle-addr' with standard method
      // (Note: Optimization was tested to be useless here)
      f_tmp = new char[f_len + (std::size_t) 1];  // For zero length separator
      std::strcpy(f_tmp, f);
      stripAngleAddress(f_tmp);
      ii = 0;  while(f_tmp[ii])  { ss[ssi++] = f_tmp[ii++]; }
      delete[] f_tmp;
      // -----------------------------------------------------------------------
      // Date
      if(!rv)
      {
         ii = 0;  while(t[ii])  { ss[ssi++] = t[ii++]; }
         ii = 0;  while(d[ii])  { ss[ssi++] = d[ii++]; }
      }
      // -----------------------------------------------------------------------
      // Line count
#if USE_LINE_COUNT
      ii = 0;  while(t[ii])  { ss[ssi++] = t[ii++]; }
      ii = 0;  while(l[ii])  { ss[ssi++] = l[ii++]; }
#endif  // USE_LINE_COUNT
      // -----------------------------------------------------------------------
      // Article number
#if USE_ARTICLE_NUMBER
      ii = 0;  while(t[ii])  { ss[ssi++] = t[ii++]; }
      ii = 0;  while(a[ii])  { ss[ssi++] = a[ii++]; }
#endif  // USE_ARTICLE_NUMBER
      // -----------------------------------------------------------------------
      // Terminate description line string
      ss[ssi] = 0;

      // Add new node with the created description line
      if(config[CONF_TVIEW].val.i)
      {
         // --------------------------------------------------------------------
         // Insert new node (threaded view)
         if(child_of_root_node)
         {
            // Special handling for children of the root node
            rti = NULL;
            for(iii = 0; iii < cti->children(); ++iii)
            {
               // Try to merge orphaned thread branches by first reference
               // (all articles with this same anchor belong to the same thread)
               h = che->child[i]->header;
               if(NULL != h->refs)
               {
                  h2 = ((core_hierarchy_element*) cti->child(iii)->user_data())
                       ->header;
                  if(NULL != h2->refs)
                  {
                     if(!strcmp(h2->refs[0], h->refs[0]))
                     {
                        rti = cti->child(iii);
                        break;
                     }
                  }
               }
            }
            if(NULL == rti)  { rti = cti; }
            nti = articleTree->add(rti, ss);
         }
         else  { nti = articleTree->add(cti, ss); }
         // --------------------------------------------------------------------
      }
      else
      {
         // --------------------------------------------------------------------
         // Insert new node as child of root node (unthreaded view)
         for(rti = articleTree->next(articleTree->first());
             rti;  rti = articleTree->next(rti))
         {
            match = false;
            if(config[CONF_UTVIEW_AN].val.i)
            {
               // Sort unthreaded view by article number
               rn = ((core_hierarchy_element*) rti->user_data())->anum;
               if(!config[CONF_INV_ORDER].val.i)
               {
                  // Normal order (older above newer)
                  if(rn >= n)  { match = true; }
               }
               else
               {
                  // Inversed order (older below newer)
                  if(rn < n)  { match = true; }
               }
            }
            else
            {
               // Sort unthreaded view by posting data
               rd = ((core_hierarchy_element*) rti->user_data())->header->date;
               if(!config[CONF_INV_ORDER].val.i)
               {
                  // Normal order (older above newer)
                  if(rd >= d_raw)  { match = true; }
               }
               else
               {
                  // Inversed order (older below newer)
                  if(rd < d_raw)  { match = true; }
               }
            }
            if(match)
            {
               nti = articleTree->insert_above(rti, ss);
               break;
            }
         }
         if(NULL == rti)  { nti = articleTree->add(articleTree->root(), ss); }
         // --------------------------------------------------------------------
      }
      nti->user_data((void*) che->child[i]);
      delete[] ss;

      // -----------------------------------------------------------------------
      // Check whether node gets an icon
      // Priority order (low to high):
      //    Positive score -> Reply to own -> Negative score -> Own
      score = filter_get_score(che->child[i]);
      // Check for positive score
      if(0 < score)  { nti->usericon(&pm_score_up); }
      // Check for reply to own article
      if(filter_match_reply_to_own(che->child[i]))
      {
         nti->usericon(&pm_reply_to_own);
      }
      // Check for negative score
      if(0 > score)
      {
         nti->usericon(&pm_score_down);
         // Mark article read
         core_mark_as_read(&group_list[group_list_index], che->child[i]->anum);
      }
      // Check for own article
      if(filter_match_own(che->child[i]))  { nti->usericon(&pm_own); }
      // -----------------------------------------------------------------------

      // Check whether article that corresponds to this node was already read
      if(core_check_already_read(&group_list[group_list_index], che->child[i]))
      {
         nti->labelfont(FL_HELVETICA);
      }

      // Kill articles with negative score below threshold
      if(kill_threshold > score)
      {
         if(main_debug)
         {
            printf("%s: %sKill article with score %d (threshold: %d)\n",
                   CFG_NAME, MAIN_ERR_PREFIX, score, kill_threshold);
         }
         articleTree->remove(nti);
         nti = cti;
      }

      // Recursively add children of current node
      if(che->child[i]->children)  { tmp = addTreeNodes(nti, che->child[i]); }
      // Check whether article was the last one read
      if(NULL == res)
      {
         if(NULL != tmp)  { res = tmp; }
         else
         {
            if(group_list[group_list_index].last_viewed == che->child[i]->anum)
            {
               res = nti;
            }
         }
      }
   }

   // Update group list
   groupListUpdateEntry(group_list_index);

   return(res);
}


// =============================================================================
// Main window update article tree widget

void  MainWindow::updateTree(void)
{
   int  rv;
   core_hierarchy_element*  che;
   Fl_Tree_Item*  rti;  // Root tree item
   Fl_Tree_Item*  child_of_root;
   Fl_Tree_Item*  sti = NULL;  // Stored (last viewed) tree item
   int  i;
   bool  unread_articles_present;
   Fl_Tree_Item*  ti;
   Fl_Tree_Item*  oi;
   bool  recalculate = true;
   bool  abort = false;

   articleTree->update_in_progress(true);

   // Redraw tree so that new internal state is recalculated
   articleTree->redraw();
   articleTree->not_drawn();
   // Wait until redraw is finished
   while(!articleTree->drawn())  { Fl::wait(0.10); }

   // Create a root item if there is none
   rti = articleTree->root();
   if(!rti)
   {
      articleTree->add("X");
      rti = articleTree->root();
   }
   // Delete all children of root item
   articleTree->clear_children(rti);

   // Redraw tree so that new internal state is recalculated
   articleTree->redraw();
   articleTree->not_drawn();
   // Wait until redraw is finished
   while(!articleTree->drawn())  { Fl::wait(0.10); }

   // Load overview lines from core article hierarchy into the tree widget
   rv = core_hierarchy_manager(NULL, CORE_HIERARCHY_GETROOT, 0, &che);
   if(!rv)  { sti = addTreeNodes(rti, che); }
   // Collapse all branches of tree
   collapseTree();

   // Delete all branches that contain no unread articles on request
   if(config[CONF_ONLYUR].val.i)
   {
      while(recalculate)
      {
         recalculate = false;
         rv = rti->children();
         for(i = 0; i < rv; ++i)
         {
            if(FL_HELVETICA_BOLD == rti->child(i)->labelfont())
            {
               unread_articles_present = true;
            }
            else
            {
               unread_articles_present
                  = checkTreeBranchForUnread(rti->child(i));
            }
            if(!unread_articles_present)
            {
               // Check whether last viewed article will be deleted
               if( rti->child(i) == sti
                   || checkTreeBranchForItem(rti->child(i), sti) )
               {
                  sti = NULL;
               }
               // This method deletes all children too
               articleTree->remove(rti->child(i));
               recalculate = true;
               break;
            }
         }
      }
      // Check for empty tree
      if(!rti->children())
      {
         ti = articleTree->add(S("No articles"));
         if(NULL != ti)
         {
            ti->deactivate();
            ti->labelfont(FL_HELVETICA);
            ti->user_data(NULL);
         }
      }
   }

   // Show the toplevel item of each branch with bold font to indicate that it
   // contains unread articles
   rv = rti->children();
   for(i = 0; i < rv; ++i)
   {
      unread_articles_present = checkTreeBranchForUnread(rti->child(i));
      if(unread_articles_present)
      {
         rti->child(i)->labelfont(FL_HELVETICA_BOLD);
      }
   }

   // Check whether there is a last viewed article stored
   if(NULL != sti)
   {
      // Select last viewed article
      ti = rti;
      // Assignment in truth expression is intended
      while(NULL != (ti = articleTree->next(ti)))
      {
         if(sti == ti)
         {
            // Open branch with last viewed article
            oi = ti;
            while(articleTree->root() != oi)
            {
               if(!articleTree->open(oi))  { articleTree->open(oi, 1); }
               for(i = 0; i < articleTree->root()->children(); ++i)
               {
                  if(articleTree->root()->child(i) == oi)
                  {
                     abort = true;
                     break;
                  }
               }
               if(abort)  { break; }
               oi = articleTree->prev(oi);
            }
            // Select last viewed article
            articleTree->set_item_focus(ti);
            articleTree->select(ti, 1);
            Fl::focus(articleTree);
            break;
         }
      }
   }
   else
   {
      // Scroll down to last (or first) branch
      if(rti->children())
      {
         if(!config[CONF_INV_ORDER].val.i)
         {
            // Last branch
            child_of_root = rti->child(rti->children() - 1);
         }
         else
         {
            // First branch (inverted order)
            child_of_root = rti->child(0);
         }
         scrollTree(UI_SCROLL_BOTTOM, child_of_root);
         articleTree->set_item_focus(child_of_root);
         Fl::focus(articleTree);
      }
      // Clear article content window
      currentArticle->text("");
      currentStyle->text("");
   }
   articleTree->redraw();

   articleTree->update_in_progress(false);
}


// =============================================================================
// Main window create article hierarchy
// (must be locked)

void  MainWindow::updateArticleTree(int  action)
{
   int  rv = -1;  // Negative: Error, 0: Success, 1: In progress, 2: Empty
   const char*  header;

   if(UI_CB_START == action)
   {
      if(!stateMachine(EVENT_AT_REFRESH))  { rv = 1; }
      else
      {
         state = 0;
         // Push horizontal scrollbar left
#ifdef FL_ABI_VERSION
#  if 10303 <= FL_ABI_VERSION
         // Requires FLTK 1.3.3
         articleTree->hposition(0);
#  endif  // 10303 <= FL_ABI_VERSION
#endif  // FL_ABI_VERSION
         // Init article hierarchy
         rv = core_hierarchy_manager(NULL, CORE_HIERARCHY_INIT, 0);
         if(!rv)
         {
            // Check whether group is empty
            if(!currentGroup->eac)  { rv = 2; }
            else
            {
               // No => Fetch 1st header
               ai = currentGroup->lwm;
               UI_STATUS(S("Update article tree ..."));
               UI_BUSY();
               // Try to fetch header overview
               ai_range.first = currentGroup->lwm;
               ai_range.last = currentGroup->hwm;
               ai_range.next = NULL;
               rv = core_get_overview(&ai_range, UI_CB_COOKIE_OVERVIEW);
               if(0 > rv)
               {
                  // No header overview available => Fetch 1st header
                  rv = core_get_article_header(&ai, UI_CB_COOKIE_HEADER);
               }
            }
         }
      }
   }
   else
   {
      if(-1 != state)
      {
         // Get result
         core_mutex_lock();
         rv = data.result;
         core_mutex_unlock();
      }
   }

   // Check return value
   if(0 >= rv || 2 == rv)
   {
      if(0 > rv)
      {
         // Failed
         clearTree();
         state = -1;
      }
      else if(2 == rv)
      {
         // Empty
         clearTree();
         state = 2;
      }
      else
      {
         // Success
         core_mutex_lock();
         header = (const char*) data.data;
         core_mutex_unlock();
         // Check whether overview is available
         if(UI_CB_FINISH == action)
         {
            core_create_hierarchy_from_overview(&group_list[group_list_index],
                                                &ai_range, header);
            core_free((void*) header);
            // Finished
            state = 1;
         }
         else
         {
            UI_PROGRESS((std::size_t) ai - currentGroup->lwm,
                        (std::size_t) currentGroup->hwm - currentGroup->lwm);
            // Check for last article
            if(currentGroup->hwm == ai++)
            {
               // Finished
               state = 1;
            }
            else
            {
               // Fetch next header in parallel using core thread
               rv = core_get_article_header(&ai, UI_CB_COOKIE_HEADER);
               if(0 > rv)  { state = -1; }
            }
            // Check whether article was canceled
            if(NULL == header)
            {
               // Yes => Mark read to achieve correct number of unread articles
               core_mark_as_read(&group_list[group_list_index], ai - 1U);
            }
            else
            {
               // No => Insert article into hierarchy
               rv = core_hierarchy_manager(NULL, CORE_HIERARCHY_ADD,
                                           ai - 1U, header);
               if(0 > rv)
               {
                  PRINT_ERROR("Adding article to hierarchy failed");
                  // Note: Don't change state here and continue
               }
               core_free((void*) header);
            }
         }
      }
      // Note: 'state' stay zero if there are more articles to fetch
      if(state)
      {
         // Reset group list widget
         groupList->deselect();
         groupList->select(groupSelect_cb_state);
         // Unlock before updating article tree widget
         if(!stateMachine(EVENT_AT_REFRESH_EXIT))
         {
            PRINT_ERROR("Error in main window state machine");
         }
         switch(state)
         {
            case 2:  // Empty
            {
               UI_STATUS(S("Group is empty"));
               break;
            }
            case 1:  // Finished
            {
               // Update UI widgets with data from core
               UI_BUSY();
               Fl::check();
               updateTree();
               UI_READY();
               UI_STATUS(S("Article tree updated."));
               break;
            }
            case -1:  // Aborted
            {
               UI_READY();
               UI_STATUS(S("Updating article tree failed."));
               break;
            }
            default:  // Bug
            {
               PRINT_ERROR("Invalid state while updating article tree");
               UI_READY();
               UI_STATUS(S("Updating article tree failed."));
               break;
            }
         }
      }
   }
}


// =============================================================================
// Main window article update

void  MainWindow::articleUpdate(Fl_Text_Buffer*  article)
{
   static bool  init = true;
   // See RFC 3986 for URI format
   const char*  url[] = { "http://", "https://", "ftp://", "nntp://",
                          "file://", "news:", "mailto:", NULL };
   const std::size_t  url_len[] = { 7, 8, 6, 7, 7, 5, 7 };
   const char  bold = 'A';   // Bold text for header field names
   const char  sig = 'B';    // Signature starting with "-- " separator
   const char  cit = 'C';    // External citation ("|" at start of line)
   const char  link = 'D';   // Hyperlink
   const char  plain = 'E';  // Normal article text
   const char  l1 = 'F';     // 1st citation level
   const char  l2 = 'G';     // 2nd citation level
   const char  l3 = 'H';     // 3rd citation level
   const char  l4 = 'I';     // 4th citation level
   Fl_Text_Buffer*  ca = currentArticle;
   char*  style;
   std::size_t  len;
   std::size_t  i;
   std::size_t  ii = 0;  // Index in current line
   std::size_t  iii = 0;
   std::size_t  iiii;
   std::size_t  url_i;
   bool  sol = true;  // Start Of Line flag
   char  hs = plain;
   bool  references = false;  // Flag indicating reference list in header
   bool  ready = false;  // Flag indicating positions beyond header separator
   int  ss = 0;
   bool  delim = false;  // Hyperlink delimiter
   bool  signature = false;  // Flag indicating signature
   bool  citation = false;  // Flag indicating external citation
   bool  hyperlink = false;  // Flag indicating hyperlink
   std::size_t  cl = 0;
   bool  cl_lock = false;
   char  c;
   int  pos = 0;

   // Store hyperlink style
   hyperlinkStyle = (unsigned int) (unsigned char) link;

   // Replace current article
   if(NULL == article)
   {
      PRINT_ERROR("Article update request without content ignored (bug)");
      return;
   }
   gui_check_article(article);
   currentArticle = article;
   text->buffer(currentArticle);
   currentLine = 0;
   if(ca)  { delete ca; }

   // Soft Hyphen (SHY) handling
   gui_process_shy(currentArticle);

   // Create article content style
   if(currentStyle)  { delete currentStyle; }
   style = currentArticle->text();
   if(NULL == style)  { len = 0; }
   else  { len = std::strlen(style); }
   if(INT_MAX < len)  { len = INT_MAX; }
   for(i = 0; i < len; ++i)
   {
      if('\n' == style[i])
      {
         sol = true;
         references = false;
         hyperlink = false;
         citation = false;
         continue;
      }
      if(hyperlink)
      {
         // Check for end of hyperlink
         // According to RFC 3986 whitespace, double quotes and angle brackets
         // are accepted as delimiters.
         // Whitespace is interpreted as SP or HT, Unicode whitespace is not
         // accepted as delimiter.
         c = style[i];
         if(' ' == c || 0x09 == (int) c || '>' == c || '"' == c)
         {
            hyperlink = false;
         }
      }
      // Highlight external citations (via '|' or '!')
      if(sol && ('|' == style[i] || '!' == style[i]))
      {
         if(!hyperlink)  { citation = true; }
      }
      // Check for start of line
      if(sol)  { sol = false;  delim = true;  ss = 0;  cl = 0;  ii = 0; }
      else  { ++ii; }
      if(signature)
      {
         // Check for end of signature in potential multipart message
         if('|' == style[i])
         {
            if(79U == ii)
            {
               for(iiii = i - ii; iiii < i; ++iiii)
               {
                  if('_' != currentArticle->byte_at((int) iiii))  { break; }
               }
               if(iiii == i)
               {
                  for(iiii = 0; iiii <= ii; ++iiii)
                  {
                     style[i - iiii] = plain;
                  }
                  signature = false;
               }
            }
         }
      }
      if(!ready)
      {
         // Check for header separator <SOL>"____...____|"
         if(!ii)
         {
            if('_' != style[i])  { hs = bold; }  else  { hs = plain; }
            iii = 0;
         }
         if('|' == style[i] && 79U <= iii)  { ready = true; }
         else if('_' == style[i])  { ++iii; }
         if(':' == style[i])  { hs = plain; }
         // Check for GS control character (reference link list marker)
         if(0x1D == (int) style[i])  { references = true; }
         if(references)
         {
            // Create hyperlinks to articles in references list
            if(0x30 <= style[i] && 0x39 >= style[i])  { hs = link; }
            else  { hs = plain; }
         }
         style[i] = hs;
      }
      else
      {
         // Check for signature separator <SOL>"-- "
         if('-' == style[i] && !ii)  { ss = 1; }
         if('-' == style[i] && 1U == ii && 1 == ss)  { ss = 2; }
         if(' ' == style[i] && 2U == ii && 2 == ss)
         {
            // Attention: This EOL check requires POSIX line format!
            if((char) 0x0A == style[i + 1U])
            {
               if(gui_last_sig_separator(&style[i + 1U]))
               {
                  style[i] = sig;  style[i - 1U] = sig;  style[i - 2U] = sig;
                  signature = true;
               }
            }
         }
         // Check for hyperlink
         url_i = 0;
         while(NULL != url[url_i])
         {
            if(!std::strncmp(&style[i], url[url_i], url_len[url_i]))
            {
               if(delim)
               {
                  style[i] = link;
                  hyperlink = true;
               }
            }
            ++url_i;
         }
         c = style[i];
         if(' ' == c || 0x09 == (int) c || '<' == c || '"' == c)
         {
            delim = true;
         }
         else  { delim = false; }
         if(1)
         {
            // Highlight citation levels of regular content
            if(!ii)
            {
               if('>' == style[i])  { cl_lock = false; }
               else  { cl_lock = true; }
            }
            if('>' == style[i] && !cl_lock)  { ++cl; }
            if('>' != style[i] && ' ' != style[i] && (char) 9 != style[i])
            {
               cl_lock = true;
            }
            if(4U < cl)  { cl = 1; }  // Rotate colors if too many levels
            switch(cl)
            {
               case 1:  { style[i] = l1;  break; }
               case 2:  { style[i] = l2;  break; }
               case 3:  { style[i] = l3;  break; }
               case 4:  { style[i] = l4;  break; }
               default:  { style[i] = plain;  break; }
            }
         }
         // Override current style for signature, citations and hyperlinks
         // (hyperlinks have highest precedence)
         if(signature)  { style[i] = sig; }
         if(citation && !signature)  { style[i] = cit; }
         if(hyperlink)  { style[i] = link; }
      }
   }
   currentStyle = new Fl_Text_Buffer((int) len, 0);
   if(NULL != style)  { currentStyle->text(style); }
   std::free((void*) style);

   // Special handling for greeting message (always display as plain text)
   if(init)  { init = false; }
   else
   {
      // Activate content style
      text->highlight_data(currentStyle, styles, styles_len, 'A', NULL, NULL);
   }

   // Replace GS marker with SP
   if (1 == currentArticle->findchar_forward(0, 0x1DU, &pos))
   {
      currentArticle->replace(pos, pos + 1, " ");
   }

   // Scroll to top/left position
   text->scroll(0, 0);
}


// =============================================================================
// Main window format and print some header fields of article

const char*  MainWindow::printHeaderFields(struct core_article_header*  h)
{
   return(gui_print_header_fields(h));
}


// =============================================================================
// Main window article selection
// (must be locked)

void  MainWindow::articleSelect(int  action)
{
   int  rv = -1;
   char*  raw;
   char*  eoh;
   const char*  p = NULL;
   const char*  q = NULL;
   Fl_Text_Buffer*  tb;
   const char*  hdr;
   bool  overview = false;
   bool  process = false;

   // Check whether current HE is available
   if(NULL == currentArticleHE)
   {
      // Print error message and terminate gracefully
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s", "Fatal error detected in 'articleSelect()' (bug)");
      exitRequest = 1;
      return;
   }

   // Check whether current HE contains only overview data
   if(CORE_HE_FLAG_OVER & currentArticleHE->flags)  { overview = true; };

   if(UI_CB_START == action)
   {
      // Check whether operation is allowed at the moment
      if(!stateMachine(EVENT_A_SELECT))  { rv = 1; }
      else if(currentArticleHE)
      {
         // Start core to fetch article body
         if(main_debug)
         {
             printf("%s: %sArticle selected\n", CFG_NAME, MAIN_ERR_PREFIX);
         }
         UI_STATUS(S("Download article ..."));
         UI_BUSY();
         startup = false;
         if(overview)
         {
            // Fetch whole article (post-fetch complete header data)
            rv = core_get_article(&currentArticleHE->anum, UI_CB_COOKIE_BODY);
         }
         else
         {
            // Fetch only body, header data in HE are already complete
            rv = core_get_article_body(&currentArticleHE->anum,
                                       UI_CB_COOKIE_BODY);
         }
      }
   }
   else
   {
      // Get result
      core_mutex_lock();
      rv = data.result;
      core_mutex_unlock();
   }

   // Check return value (positive value means "in progress")
   if(0 >= rv)
   {
      UI_READY();
      if(!rv)
      {
         process = true;
         core_mutex_lock();
         raw = (char*) data.data;
         core_mutex_unlock();
         p = raw;
         // Reset wrap mode
         wrapMode = Fl_Text_Display::WRAP_NONE;
         text->wrap_mode(wrapMode, 0);
         // Extract header data and update HE if required
         if(!overview)  { q = p; }
         else
         {
            // Search for end of header
            // Attention: Overloaded and both prototypes different than in C!
            eoh = std::strstr(raw, "\r\n\r\n");
            if(NULL == eoh)
            {
               core_free((void*) p);
               process = false;
            }
            else
            {
               // Set pointer to body data
               q = &eoh[4];
               // Update HE
               eoh[2] = 0;
               rv = core_hierarchy_manager(NULL, CORE_HIERARCHY_UPDATE,
                                           currentArticleHE->anum, p);
               if(0 > rv)  { process = false; }
            }
         }
      }
      if(!process)
      {
         // Failed
         UI_STATUS(S("Download of article failed."));
         PRINT_ERROR("Processing of article failed");
         // Clear article content window
         currentArticle->text("");
         currentStyle->text("");
      }
      else
      {
         // Success, update article window
         UI_STATUS(S("Article successfully downloaded."));
         // Create text buffer
         tb = new Fl_Text_Buffer(0, 0);
#if 0
         // For debugging
         // The 'printf()' implementation must be able to handle NULL pointers!
         std::printf("\nArticle watermark ............: %lu\n",
                     currentArticleHE->anum);
         std::printf("MIME version .................: %s\n",
                     currentArticleHE->header->mime_v);
         std::printf("MIME content transfer encoding: %s\n",
                     currentArticleHE->header->mime_cte);
         std::printf("MIME content type ............: %s\n",
                     currentArticleHE->header->mime_ct);
#endif
         // Print formatted header data to text buffer
         hdr = printHeaderFields(currentArticleHE->header);
         if(NULL != hdr)
         {
            tb->text(hdr);
            delete[] hdr;
         }
         // Print header/body delimiter
         tb->append(ENC_DELIMITER);
         // Create MIME object from content
         if(NULL != mimeData)  { delete mimeData; }
         mimeData = new MIMEContent(currentArticleHE->header, q);
         core_free((void*) p);  p = NULL;
         gui_decode_mime_entities(tb, mimeData,
                                  currentArticleHE->header->msgid);
         // Print content
         articleUpdate(tb);
         // Store last viewed article
         group_list[group_list_index].last_viewed = currentArticleHE->anum;
      }
      // Mark current article read
      core_mark_as_read(&group_list[group_list_index],
                        currentArticleHE->anum);
      groupListUpdateEntry(group_list_index);
      // Check for error
      if(!process)  { currentArticleHE = NULL; }
      else
      {
         // Reset search start position
         currentSearchPosition = 0;
      }
      if(!stateMachine(EVENT_A_SELECT_EXIT))
      {
         PRINT_ERROR("Error in main window state machine");
      }
   }
}


// =============================================================================
// Main window view message of the day callback
// (must be locked)

void  MainWindow::viewMotd(int  action)
{
   int  rv = -1;
   std::ostringstream  titleString;
   const char*  motd = NULL;
   const char*  p;

   if(UI_CB_START == action)
   {
      if(!stateMachine(EVENT_MOTD_VIEW))  { rv = 1; }
      else
      {
         // Start core to do the work
         UI_STATUS(S("Get message of the day ..."));
         UI_BUSY();
         rv = core_get_motd(UI_CB_COOKIE_MOTD);
      }
   }
   else
   {
      // Get result
      core_mutex_lock();
      rv = data.result;
      core_mutex_unlock();
   }

   // Check return value (positive value means "in progress")
   if(0 >= rv)
   {
      UI_READY();
      if(0 > rv)  { UI_STATUS(S("No message of the day or download failed.")); }
      else
      {
         // Success
         core_mutex_lock();
         p = (const char*) data.data;
         core_mutex_unlock();
         UI_STATUS(S("Message of the day successfully downloaded."));

         // Verify UTF-8 encoding and convert to POSIX form
         if(enc_uc_check_utf8(p))
         {
            motd = core_convert_canonical_to_posix("[Invalid encoding]\r\n",
                                                   1, 0);
         }
         else  { motd = core_convert_canonical_to_posix(p, 1, 0); }

         // Create Unicode title for window
         SC("")
         SC("Do not use characters for the translation that cannot be")
         SC("converted to the ISO 8859-1 character set for this item.")
         SC("Leave the original string in place if in doubt.")
         titleString << S("Message of the day") << std::flush;
         SC("")

         // Attention:
         //
         //    const char*  title = titleString.str().c_str()
         //
         // creates 'title' with undefined value because the compiler is allowed
         // to free the temporary string object returned by 'str()' immediately
         // after the assignment!
         // Assigning names to temporary string objects forces them to stay in
         // memory as long as their names go out of scope (this is what we
         // need).
         const std::string&  ts = titleString.str();

         // Set "Article source code" window title
#if CFG_USE_XSI && !CFG_NLS_DISABLE
         // Convert window title to the encoding required by the window manager
         // (WM)
         // It is assumed that the WM can display either ISO 8859-1 or UTF-8
         // encoded window titles.
         const char*  title;
         title = gui_utf8_iso(ts.c_str());
         if(NULL != title)
         {
            // Display "Message of the day" window
            new MotdWindow(motd, title);
            // Release memory for window title
            delete[] title;
         }
#else  // CFG_USE_XSI && !CFG_NLS_DISABLE
         // Display "Message of the day" window
         new MotdWindow(motd, ts.c_str());
#endif  // CFG_USE_XSI && !CFG_NLS_DISABLE
         // Release memory for raw article content
         enc_free((void*) motd);
         core_free((void*) p);
      }
      if(!stateMachine(EVENT_MOTD_VIEW_EXIT))
      {
         PRINT_ERROR("Error in main window state machine");
      }
   }
}


// =============================================================================
// Main window view article callback
// (must be locked)
//
// Note: The parameter mid must be specified without angle brackets!

void  MainWindow::viewArticle(int  action, const char*  mid)
{
   static const char  debug_prefix[] = "Try to fetch article: ";
   int  rv = -1;
   std::ostringstream  titleString;
   const char*  article;
   char*  sbuf;
   std::size_t  len;

   if(UI_CB_START == action)
   {
      if(!stateMachine(EVENT_A_VIEW))  { rv = 1; }
      else if(NULL != mid)
      {
         // Start core to do the work
         UI_STATUS(S("Get article ..."));
         UI_BUSY();
         // Add angle brackets
         mid_a = new char[std::strlen(mid) + (std::size_t) 3];
         mid_a[0] = '<';
         std::strcpy(&mid_a[1], mid);
         std::strcat(mid_a, ">");
         if(main_debug)
         {
            len = std::strlen(MAIN_ERR_PREFIX);
            len += std::strlen(debug_prefix);
            len += std::strlen(mid_a);
            sbuf = new char[len + (std::size_t) 1];
            std::strcpy(sbuf, MAIN_ERR_PREFIX);
            std::strcat(sbuf, debug_prefix);
            std::strcat(sbuf, mid_a);
            print_error(sbuf);
            delete[] sbuf;
         }
         rv = core_get_article_by_mid(mid_a, UI_CB_COOKIE_ARTICLE);
      }
   }
   else
   {
      // Get result
      core_mutex_lock();
      rv = data.result;
      core_mutex_unlock();
      // Free memory for Message-ID with angle brackets
      delete[] mid_a;
   }

   // Check return value (positive value means "in progress")
   if(0 >= rv)
   {
      UI_READY();
      if(0 > rv)
      {
         UI_STATUS(S("Downloading article failed."));
         SC("Do not use non-ASCII for the translation of this item")
         fl_message_title(S("Error"));
         fl_alert("%s", S("Article not found"));
      }
      else
      {
         // Success
         core_mutex_lock();
         article = (const char*) data.data;
         core_mutex_unlock();
         UI_STATUS(S("Article successfully downloaded."));

         // Create Unicode title for article window
         SC("")
         SC("Do not use characters for the translation that cannot be")
         SC("converted to the ISO 8859-1 character set for this item.")
         SC("Leave the original string in place if in doubt.")
         titleString << S("Article") << std::flush;
         SC("")

         // Attention:
         //
         //    const char*  title = titleString.str().c_str()
         //
         // creates 'title' with undefined value because the compiler is allowed
         // to free the temporary string object returned by 'str()' immediately
         // after the assignment!
         // Assigning names to temporary string objects forces them to stay in
         // memory as long as their names go out of scope (this is what we
         // need).
         const std::string&  ts = titleString.str();

         // Set "Article" window title
#if CFG_USE_XSI && !CFG_NLS_DISABLE
         // Convert window title to the encoding required by the window manager
         // (WM)
         // It is assumed that the WM can display either ISO 8859-1 or UTF-8
         // encoded window titles.
         const char*  title;
         title = gui_utf8_iso(ts.c_str());
         if(NULL != title)
         {
            // Display "Article" window
            new ArticleWindow(article, title);
            // Release memory for window title
            delete[] title;
         }
#else  // CFG_USE_XSI && !CFG_NLS_DISABLE
         // Display "Article" window
         new ArticleWindow(article, ts.c_str());
#endif  // CFG_USE_XSI && !CFG_NLS_DISABLE
         // Release memory for raw article content
         core_free((void*) article);
      }
      if(!stateMachine(EVENT_A_VIEW_EXIT))
      {
         PRINT_ERROR("Error in main window state machine");
      }
   }
}


// =============================================================================
// Main window view article source code callback
// (must be locked)

void  MainWindow::viewSrc(int  action)
{
   int  rv = -1;
   std::ostringstream  titleString;
   const char*  article;

   if(UI_CB_START == action)
   {
      if(!stateMachine(EVENT_SRC_VIEW))  { rv = 1; }
      else if(NULL != currentArticleHE)
      {
         // Start core to do the work
         UI_STATUS(S("Get article source code ..."));
         UI_BUSY();
         rv = core_get_article(&currentArticleHE->anum, UI_CB_COOKIE_SRC);
      }
   }
   else
   {
      // Get result
      core_mutex_lock();
      rv = data.result;
      core_mutex_unlock();
   }

   // Check return value (positive value means "in progress")
   if(0 >= rv)
   {
      UI_READY();
      if(0 > rv)  { UI_STATUS(S("Downloading article source code failed.")); }
      else
      {
         // Success
         core_mutex_lock();
         article = (const char*) data.data;
         core_mutex_unlock();
         UI_STATUS(S("Article source code successfully downloaded."));

         // Create Unicode title for article source code window
         SC("")
         SC("Do not use characters for the translation that cannot be")
         SC("converted to the ISO 8859-1 character set for this item.")
         SC("Leave the original string in place if in doubt.")
         titleString << S("Article source code") << std::flush;
         SC("")

         // Attention:
         //
         //    const char*  title = titleString.str().c_str()
         //
         // creates 'title' with undefined value because the compiler is allowed
         // to free the temporary string object returned by 'str()' immediately
         // after the assignment!
         // Assigning names to temporary string objects forces them to stay in
         // memory as long as their names go out of scope (this is what we
         // need).
         const std::string&  ts = titleString.str();

         // Set "Article source code" window title
#if CFG_USE_XSI && !CFG_NLS_DISABLE
         // Convert window title to the encoding required by the window manager
         // (WM)
         // It is assumed that the WM can display either ISO 8859-1 or UTF-8
         // encoded window titles.
         const char*  title;
         title = gui_utf8_iso(ts.c_str());
         if(NULL != title)
         {
            // Display "Article source code" window
            new ArticleSrcWindow(article, title);
            // Release memory for window title
            delete[] title;
         }
#else  // CFG_USE_XSI && !CFG_NLS_DISABLE
         // Display "Article source code" window
         new ArticleSrcWindow(article, ts.c_str());
#endif  // CFG_USE_XSI && !CFG_NLS_DISABLE
         // Release memory for raw article content
         core_free((void*) article);
      }
      if(!stateMachine(EVENT_SRC_VIEW_EXIT))
      {
         PRINT_ERROR("Error in main window state machine");
      }
   }
}


// =============================================================================
// Main window article compose callback
// (must be locked)
//
// reply / super  Action
// ----------------------------------------------
// false / false  Create a new message/thread
// false /  true  Create a cancel control message
//  true / false  Create a followup message
//  true /  true  Create a superseding message

void  MainWindow::articleCompose(bool  reply, bool  super)
{
   const char*  fqdn = config[CONF_FQDN].val.s;
   std::ostringstream  titleString;
   char*  p = NULL;
   char*  q = NULL;
   const char*  cq = NULL;
   std::size_t  i = 0;
   char*  sig_delim;
   char*  sig = NULL;
   struct core_article_header*  hdr = NULL;
   char*  from = NULL;
   std::size_t  len;
   Fl_Text_Buffer  header;
   const char*  msgid = NULL;
   const char*  ckey1 = NULL;
   const char*  ckey = NULL;
   const char*  subject;
   const char*  datetime;
   bool  headerOK = true;
   int  rv;
   int  start;
   int  end;
   unsigned int  c = (unsigned int) '\n';
   int  pos;
   const char*  field_name;
   const char*  field;
   const char*  field2;
   bool  insertedMessageID = false;
   int  inspos;
   bool  abort = true;
   bool  error = true;

   // Allow only one composer at a time
   if(!stateMachine(EVENT_COMPOSE))  { return; }

   if(NULL == currentGroup)
   {
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s", S("No group selected"));
   }
   else if((reply || super) && NULL == currentArticleHE)
   {
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s", S("No article selected"));
   }
   else
   {
      if(reply || super)  { hdr = currentArticleHE->header; }
      abort = false;
   }
   if(!abort)
   {
      // Check for special exception Fup2 poster
      if(reply && !super && NULL != hdr->fup2)
      {
         if(!std::strcmp("poster", hdr->fup2))
         {
            SC("Do not use non-ASCII for the translation of this item")
            fl_message_title(S("Warning"));
            rv = fl_choice("%s", S("Cancel"),
                           S("OK"),
                           S("Ignore"),
                           S("Really execute Followup-To to poster?"));
            if(1 == rv)  { sendEmail(); }  // OK
            if(!rv || 1 == rv)  { abort = true; }
         }
      }
   }
   if(!abort)
   {
      if(reply)
      {
         // Prepare current article content for citation
         if(currentArticle->selected())
         {
            // Some text was selected, use only this part
            p = currentArticle->selection_text();
            if(NULL != p)  { cq = p; }
         }
         else
         {
            // Use complete article body
            p = currentArticle->text();
            if(NULL != p)
            {
               // Strip header
               while('_' != p[i++]);
               while('|' != p[i++]);
               while('|' != p[i++]);
               q = &p[++i];
               cq = q;
               // Strip signature
               while(1)
               {
                  // Attention: Overloaded and both prototypes different than in C!
                  sig_delim = std::strstr(q, "\n-- \n");
                  if(NULL == sig_delim)  { break; }
                  else { sig = q = &sig_delim[1]; }
               }
               if(NULL != sig)  { sig[0] = 0; }
            }
         }
         // Extract author name
         len = std::strlen(hdr->from);
         from = new char[++len];
         std::strcpy(from, hdr->from);
         stripAngleAddress(from);
      }
      else
      {
         if(super)
         {
            // Prepare default cancel reason text
            cq = "Reason for cancel unknown.\n";
         }
         else if(std::strlen(config[CONF_INITIAL_GREETING].val.s))
         {
            // Prepare initial greeting (if configured)
            cq = config[CONF_INITIAL_GREETING].val.s;
         }
      }

      // Create message header
      //
      // According to RFC 5536 the following rules are applied:
      // - Arbitrary ordering of header fields is allowed => We use common one
      // - Header field name must be followed by a colon and a space => We do so
      //
      // According to RFC 5537 the following rules are applied:
      // - Mandatory fields for proto article: From, Newsgroups, Subject
      //   => We create at least these header fields
      // - The header field References must be no longer than 998 octets
      //   after unfolding => We trim the references if this is the case
      header.append("Path: not-for-mail\n");
      if(std::strlen(fqdn))
      {
         header.append("Message-ID: ");
         msgid = core_get_msgid(fqdn);
         if(NULL == msgid)  { headerOK = false; }
         else
         {
            header.append(msgid);
            insertedMessageID = true;
         }
         // Memory is released later because of Cancel-Lock!
         header.append("\n");
      }
      if(!std::strlen(config[CONF_FROM].val.s))
      {
         SC("Do not use non-ASCII for the translation of this item")
         fl_message_title(S("Error"));
         fl_alert("%s", S("From: header field not configured"));
         headerOK = false;
      }
      else
      {
         field_name = "From: ";
         field = enc_create_name_addr(config[CONF_FROM].val.s,
                                      std::strlen(field_name));
         if(NULL == field)  { headerOK = false; }
         else
         {
            header.append(field_name);
            header.append(field);
            header.append("\n");
            // Only allow supersede or cancel for own messages
            if(super)
            {
               field2 = enc_create_name_addr(hdr->from,
                                             std::strlen(field_name));
               if(NULL == field2)  { headerOK = false; }
               else
               {
                  if(std::strcmp(field, field2))
                  {
                     SC("Do not use non-ASCII for the translation of this item")
                     fl_message_title(S("Error"));
                     fl_alert("%s", S("Supersede or cancel not allowed"));
                     headerOK = false;
                  }
                  enc_free((void*) field2);
               }
            }
            enc_free((void*) field);
         }
      }
      header.append("Newsgroups: ");
      if(!super && !reply)  { header.append(currentGroup->name); }
      else
      {
         len = 12;
         // Check for Fup2 header field
         rv = 0;
         if(!super && NULL != hdr->fup2)
         {
            if(std::strcmp("poster", hdr->fup2))
            {
               SC("Do not use non-ASCII for the translation of this item")
               fl_message_title(S("Note"));
               fl_message("%s", S("Followup-To specified groups executed"));
               header.append(hdr->fup2);
               len += std::strlen(hdr->fup2);
               rv = 1;
            }
         }
         if(!rv)
         {
            // Not found => Use current group list
            i = 0;
            do
            {
               if(i)  { header.append(","); }
               ++len;
               header.append(hdr->groups[i]);
               len += std::strlen(hdr->groups[i]);
            }
            while(NULL != hdr->groups[++i]);
         }
         // Verify length but never fold "Newsgroups" header field
         if((std::size_t) 998 < len)  { headerOK = false; }
      }
      header.append("\n");
      header.append("Subject: ");
      if(reply)
      {
         if(super)  { subject = hdr->subject; }
         else
         {
            header.append("Re: ");
            subject = gui_check_re_prefix(hdr->subject);
         }
         header.append(subject);
      }
      else if(super)
      {
         // Old behaviour until 0.14
         //header.append(hdr->subject);
         // New behaviour since 0.15 (backward compatible to RFC 1036 syntax)
         if(NULL == hdr->msgid)  { headerOK = false; }
         else
         {
            header.append("cmsg cancel ");
            header.append(hdr->msgid);
         }
      }
      header.append("\n");
      if(!super)
      {
         // Strip substring starting with "(was:" matched case insensitive
         rv = header.search_forward(0, "Subject:", &start, 1);
         if(1 == rv)
         {
            // Name match found => Verify that it starts at BOL
            if(0 < start)  { c = header.char_at(start - 1); }
            if((unsigned int) '\n' == c)
            {
               // Yes => Search for EOL
               rv = header.findchar_forward(start, (unsigned int) '\n', &end);
               if(1 == rv)
               {
                  rv = header.search_forward(start, "(was:", &pos, 0);
                  if(1 == rv)
                  {
                     header.remove(pos, end);
                     if(pos)
                     {
                        if((unsigned int) ' ' == header.char_at(pos - 1))
                        {
                           header.remove(pos - 1, pos);
                        }
                     }
                  }
               }
            }
         }
      }
      header.append("Date: ");
      datetime = core_get_datetime(0);
      if(NULL == datetime)  { headerOK = false; }
      else  { header.append(datetime); }
      header.append("\n");
      // --- Optional header fields ---
      if(insertedMessageID)
      {
         // Create Cancel-Lock header if we have created the Message-ID
         ckey1 = core_get_cancel_lock(CORE_CL_SHA1, msgid);
         ckey = core_get_cancel_lock(CORE_CL_SHA256, msgid);
         if(NULL != ckey1 || NULL != ckey)
         {
            header.append("Cancel-Lock: ");
            if(NULL != ckey1)  { header.append(ckey1); }
            if(NULL != ckey1 && NULL != ckey)  { header.append(" " ); }
            if(NULL != ckey)  { header.append(ckey); }
            header.append("\n");
         }
         core_free((void*) ckey);
         core_free((void*) ckey1);
         // RFC 5536 requires that this header is inserted at injection.
         // According to RFC 5537 an injecting agent is not allowed to add this
         // header field to a proto-article that already contains "Message-ID"
         // and "Date" header-fields. Therefore, if we have added these two
         // header-fields, we add "Injection-Date" too.
         header.append("Injection-Date: ");
         header.append(datetime);
         header.append("\n");
      }
      core_free((void*) datetime);
      core_free((void*) msgid);  // Released here because of Cancel-Lock
      if(super)
      {
         msgid = hdr->msgid;
         if(NULL == msgid)  { headerOK = false; }
         else
         {
            if(reply)
            {
               header.append("Supersedes: ");
               header.append(msgid);
               header.append("\n");
            }
            else
            {
               header.append("Control: cancel ");
               header.append(msgid);
               header.append("\n");
            }
            if(insertedMessageID)
            {
               // Create Cancel-Key header if we have created the Message-ID
               ckey1 = core_get_cancel_key(CORE_CL_SHA1, msgid);
               ckey = core_get_cancel_key(CORE_CL_SHA256, msgid);
               if(NULL != ckey1 || NULL != ckey)
               {
                  header.append("Cancel-Key: ");
                  if(NULL != ckey1)  { header.append(ckey1); }
                  if(NULL != ckey1 && NULL != ckey)  { header.append(" " ); }
                  if(NULL != ckey)  { header.append(ckey); }
                  header.append("\n");
               }
               core_free((void*) ckey1);
               core_free((void*) ckey);
            }
         }
      }
      if(std::strlen(config[CONF_REPLYTO].val.s))
      {
         field_name = "Reply-To: ";
         field = enc_create_name_addr(config[CONF_REPLYTO].val.s,
                                      std::strlen(field_name));
         if(NULL == field)  { headerOK = false; }
         else
         {
            header.append(field_name);
            header.append(field);
            header.append("\n");
            enc_free((void*) field);
         }
      }
      // According to RFC 5537 the maximum length of the References header
      // field must be trimmed to 998 octets. Because Message-IDs are not
      // allowed to contain MIME encoded words, folding of this header
      // field is never required.
      if(reply)
      {
         len = 0;
         inspos = 0;
         // Add new reference for parent if not superseding
         if(!(NULL == hdr->refs && super))
         {
            header.append("References: ");
            inspos = header.length();
            len = 12;
            header.append("\n");
         }
         if(!super)
         {
            header.insert(inspos, hdr->msgid);
            len += std::strlen(hdr->msgid);
         }
         if(NULL != hdr->refs)
         {
            if(NULL != hdr->refs[0])
            {
               // Count first previous reference
               ++len;
               len += std::strlen(hdr->refs[0]);
               // Insert previous references (backward, beginning with last one)
               i = 0;
               while(NULL != hdr->refs[i])  { ++i; }
               while(--i)
               {
                  len += std::strlen(hdr->refs[i]);
                  if(UI_HDR_BUFSIZE < ++len)  { break; }
                  else
                  {
                     header.insert(inspos, " ");
                     header.insert(inspos, hdr->refs[i]);
                  }
               }
               // Always insert first previous reference
               header.insert(inspos, " ");
               header.insert(inspos, hdr->refs[0]);
            }
         }
      }
      if(std::strlen(config[CONF_ORGANIZATION].val.s))
      {
         header.append("Organization: ");
         header.append(config[CONF_ORGANIZATION].val.s);
         header.append("\n");
      }
      if(config[CONF_ENABLE_UAGENT].val.i)
      {
         header.append("User-Agent: " CFG_NAME "/" CFG_VERSION
                       " (for " CFG_OS ")\n");
      }
      // Insert MIME header field
      // Note: It is mandatory for a MIME conformant client to send this header
      // field with all messages!
      header.append("MIME-Version: 1.0\n");
      // Insert header/body separator (empty line)
      header.append("\n");

      // Ensure that header was successfully created
      if(true != headerOK)
      {
         SC("Do not use non-ASCII for the translation of this item")
         fl_message_title(S("Error"));
         fl_alert("%s", S("Creating message header failed"));
      }
      else
      {
         const char*  header_text;
         header_text = header.text();
         if(NULL != header_text)
         {
            // Create Unicode title for compose window
            SC("")
            SC("Do not use characters for the translation that cannot be")
            SC("converted to the ISO 8859-1 character set for the items")
            SC("in this section.")
            SC("Leave the original strings in place if in doubt.")
            if(reply)
            {
               titleString << S("Compose followup or reply") << std::flush;
            }
            else  { titleString << S("Compose new article") << std::flush; }
            SC("")
            // Attention:
            //
            //    const char*  title = titleString.str().c_str()
            //
            // creates 'title' with undefined value because the compiler is allowed
            // to free the temporary string object returned by 'str()' immediately
            // after the assignment!
            // Assigning names to temporary string objects forces them to stay in
            // memory as long as their names go out of scope (this is what we
            // need).
            const std::string&  ts = titleString.str();

            // Construct compose window
#if CFG_USE_XSI && !CFG_NLS_DISABLE
            // Convert window title to the encoding required by the window manager
            // (WM)
            // It is assumed that the WM can display either ISO 8859-1 or UTF-8
            // encoded window titles.
            const char*  title;
            title = gui_utf8_iso(ts.c_str());
            if(NULL != title)
            {
               // Construct article composer
               composeWindow = new ComposeWindow(title, header_text, cq, from,
                                                 hdr, super);
               error = false;
               // Release memory for window title
               delete[] title;
            }
#else  // CFG_USE_XSI && !CFG_NLS_DISABLE
            // Construct article composer
            composeWindow = new ComposeWindow(ts.c_str(), header_text, cq, from,
                                              hdr, super);
            error = false;
#endif  // CFG_USE_XSI && !CFG_NLS_DISABLE
         }
         std::free((void*) header_text);
      }
   }

   // Release memory
   if(NULL != from)  { delete[] from; }
   std::free((void*) p);

   // Verify that compose windows was constructed
   if(error)
   {
      if(!stateMachine(EVENT_COMPOSE_EXIT))
      {
         PRINT_ERROR("Error in main window state machine");
      }
   }
}


// =============================================================================
// Main window article posting callback
// (must be locked)
//
// The pointer \e article must point to a memory block allocated by \c malloc()
// and this function will call \c free(article) .

void  MainWindow::articlePost(int  action, const char*  article)
{
   int  rv = -1;
   const char*  p;
   enum fm
   {
      FM_DEFAULT,
      FM_CORE,
      FM_EXT
   }
   free_method;

   free_method = FM_DEFAULT;
   if(UI_CB_START == action)
   {
      if(!stateMachine(EVENT_POST))  { rv = 1; }
      else
      {
         UI_STATUS(S("Post article ..."));
         UI_BUSY();
         // Convert article content line breaks from POSIX form to canonical
         // (RFC 822) form
         p = core_convert_posix_to_canonical(article);
         if(NULL == p)
         {
            SC("Do not use non-ASCII for the translation of this item")
            fl_message_title(S("Error"));
            fl_alert("%s", S("Conversion from local to canonical form failed"));
         }
         else
         {
            // Success
            if(p != article)
            {
               std::free((void*) article);
               article = p;
               free_method = FM_CORE;
            }
            // Call external postprocessor (while blocking UI)
            // The body of the article always has Unicode format
            p = ext_pp_filter(article);
            if(NULL == p)
            {
               SC("Do not use non-ASCII for the translation of this item")
               fl_message_title(S("Error"));
               fl_alert("%s", S("External postprocessor failed"));
            }
            else
            {
               // Success
               if(p != article)
               {
                  if(FM_CORE == free_method)  { core_free((void*) article); }
                  else  { std::free((void*) article); }
                  article = p;
                  free_method = FM_EXT;
               }
               // Start core to do the work
               rv = core_post_article(article, UI_CB_COOKIE_POST);
            }
         }
      }
      // Release memory
      switch(free_method)
      {
         case FM_CORE:
         {
            core_free((void*) article);
            break;
         }
         case FM_EXT:
         {
            ext_free((void*) article);
            break;
         }
         default:
         {
            std::free((void*) article);
            break;
         }
      }
   }
   else
   {
      // Get result
      core_mutex_lock();
      rv = data.result;
      core_mutex_unlock();
   }

   // Check return value (positive value means "in progress")
   if(0 >= rv)
   {
      UI_READY();
      if(!stateMachine(EVENT_POST_EXIT))
      {
         PRINT_ERROR("Error in main window state machine");
      }
      if(0 > rv)
      {
         UI_STATUS(S("Posting article failed."));
         SC("Do not use non-ASCII for the translation of this item")
         fl_message_title(S("Error"));
         fl_alert("%s", S("Posting article failed."));
      }
      else
      {
         // Success
         UI_STATUS(S("Article successfully posted."));
         if(NULL != composeWindow)
         {
            delete composeWindow;
            composeWindow = NULL;
            // Call this after EVENT_POST_EXIT
            composeComplete();
         }
      }
   }
}


// =============================================================================
// Main window article tree search (for Message-ID)
// The target article is selected, if found.
//
// target_mid must point to the first character after the opening angle bracket
// tlen must be the size of the Message-ID without angle brackets.

Fl_Tree_Item*  MainWindow::searchSelectArticle(const char*  target_mid,
                                               std::size_t  tlen)
{
   Fl_Tree_Item*  ti;
   Fl_Tree_Item*  oi;
   const char*  mid;
   bool  abort = false;
   int  i;

   ti = articleTree->root();
   // Assignment in truth expression is intended
   while(NULL != (ti = articleTree->next(ti)))
   {
      mid = ((core_hierarchy_element*) ti->user_data())->header->msgid;
      if(std::strlen(mid) != tlen + (std::size_t) 2)
      {
         continue;
      }
      // Note: The Message-ID from header has angle brackets
      if(!std::strncmp(&mid[1], target_mid, tlen))
      {
         // Open branch containing target article
         oi = ti;
         while(articleTree->root() != oi)
         {
            if(!articleTree->open(oi))
            {
               articleTree->open(oi, 1);
            }
            for(i = 0; i < articleTree->root()->children(); ++i)
            {
               if(articleTree->root()->child(i) == oi)
               {
                  abort = true;
                  break;
               }
            }
            if(abort)  { break; }
            oi = articleTree->prev(oi);
         }
         // Select last viewed article
         articleTree->set_item_focus(ti);
         articleTree->deselect_all();
         articleTree->select(ti, 1);
         Fl::focus(articleTree);
         break;
      }
   }

   return(ti);
}


// =============================================================================
// Store MIME entity to file

void  MainWindow::storeMIMEEntityToFile(const char*  uri, const char*  msgid)
{
   const char*  link = NULL;
   const char*  start = NULL;
   std::size_t  len = 0;
   const char*  p;
   unsigned int  i;
   int  error = 1;
   char*  name = NULL;
   const char*  suggest = NULL;

   // Check whether link points to our MIME entities (ignore entity index)
   link = gui_create_link_to_entity(msgid, 0);
   if(NULL != link && link[0])
   {
      // Attention: 'link' is created with angle brackets and dummy entity index
      start = &link[1];
      p = std::strrchr(start, (int) '/');
      if(NULL != p)
      {
         len = (size_t) (p - start);
         error = 0;
      }
   }
   if(main_debug)
   {
      if(error)
      {
         fprintf(stderr, "%s: %sCannot check link to MIME entity (bug)\n",
                 CFG_NAME, MAIN_ERR_PREFIX);
      }
      else
      {
         fprintf(stderr, "%s: %sClick on link to MIME entity detected:\n",
                 CFG_NAME, MAIN_ERR_PREFIX);
         fprintf(stderr, "%s: %s%s (URI)\n", CFG_NAME, MAIN_ERR_PREFIX, uri);
         fprintf(stderr, "%s: %s", CFG_NAME, MAIN_ERR_PREFIX);
         for(i = 0; len > (std::size_t) i; ++i)
         {
            fprintf(stderr, "%c", start[i]);
         }
         fprintf(stderr, " (Compare, Length: %u)\n", (unsigned int) len);
      }
   }
   // Check whether links differ before last "/"
   if(error || std::strncmp(uri, start, len))
   {
      // Yes (or error)
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s", S("Link target not found or not supported"));
   }
   else
   {
      // Attention: Overloaded and both prototypes different than in C!
      p = std::strrchr(uri, (int) '/');
      if(NULL == p || 1 != std::sscanf(++p, "%u", &i))
      {
         PRINT_ERROR("Extracting MIME entity number from file URI failed");
      }
      else
      {
         enc_mime_cte  cte = ENC_CTE_UNKNOWN;
         const char*  entity_body = mimeData->part(i, &cte, NULL);
         if(NULL != entity_body)
         {
            SC("Do not use characters for the translation that cannot be")
            SC("converted to the ISO 8859-1 character set for this item.")
            SC("Leave the original string in place if in doubt.")
            const char*  title = S("Save attachment");
            const char*  homedir = core_get_homedir();
            if(NULL != homedir)
            {
               if(NULL != mimeData->filename(i))
               {
                  // Use filename from Content-Disposition header field
                  name = (char*) std::malloc(std::strlen(homedir)
                                            + std::strlen(mimeData->filename(i))
                                            + (size_t) 2);
                  if(NULL != name)
                  {
                     suggest = name;
                     std::strcpy(name, homedir);
                     std::strcat(name, "/");
                     std::strcat(name, mimeData->filename(i));
                  }
               }
               else
               {
                  // Use generic filename
                  suggest = core_suggest_pathname();
               }
               if (NULL != suggest)
               {
                  fl_file_chooser_ok_label(S("Save"));
                  const char*  pathname =
                     fl_file_chooser(title, "*", suggest, 0);
                  if(NULL != pathname)
                  {
                     // Convert pathname encoding to locale
                     const char*  pn =
                        core_convert_pathname_to_locale(pathname);
                     if(NULL == pn)
                     {
                     SC("Do not use non-ASCII for the translation of this item")
                        fl_message_title(S("Error"));
                        fl_alert("%s",
                             S("Pathname conversion to locale codeset failed"));
                     }
                     else
                     {
                       // Decode Content-Transfer-Encoding and save body to file
                        int rv = enc_mime_save_to_file(pn, cte, entity_body);
                        if(rv)
                        {
                     SC("Do not use non-ASCII for the translation of this item")
                           fl_message_title(S("Error"));
                           fl_alert("%s", S("Operation failed"));
                        }
                        core_free((void*) pn);
                     }
                  }
               }
               core_free((void*) homedir);
            }
         }
      }
   }
   std::free((void*) link);
   std::free((void*) suggest);
}


// =============================================================================
// Main window hyperlink handling callback

void  MainWindow::hyperlinkHandler(int  pos)
{
   const char*  mailto = "mailto:";
   const char*  news = "news:";
   const char*  nntp = "nntp:";
   const char*  file = "file:";
   char*  uri = NULL;
   int  rv;
   int  i = pos;
   int  start = pos;
   int  end = start;
   std::size_t  len;
   Fl_Tree_Item*  ti;
   bool  abort = false;
   std::size_t  clen;
   std::size_t  gi;
   core_anum_t  ref_i;
   const char*  p;
   char*  q;
   int  invalid;

   //std::printf("Debug: Hyperlink clicked at position: %d\n", pos);
   if(currentArticle && currentStyle)
   {
      // Extract complete URI
      while(i <= pos)
      {
         i = currentArticle->prev_char(i);
         if(currentStyle->char_at(i) == hyperlinkStyle)  { start = i; }
         else  { break; }
      }
      i = pos;
      while(i >= pos)
      {
         i = currentArticle->next_char(i);
         if(currentStyle->char_at(i) == hyperlinkStyle)  { end = i; }
         else  { ++end;  break; }
      }
      uri = currentArticle->text_range(start, end);
      if(NULL != uri)
      {
         // Check for "nntp:" URI
         len = std::strlen(nntp);
         if(std::strlen(uri) >= len && !std::strncmp(uri, nntp, len))
         {
            // Yes => Not supported
            PRINT_ERROR("Multi server support missing for nntp URI type");
            SC("Do not use non-ASCII for the translation of this item")
            fl_message_title(S("Error"));
            fl_alert("%s", S("URI type is not supported"));
         }
         else
         {
            // Check for "news:" URI
            // According to RFC 1738 this specifies a newsgroup or a Message-ID
            // An asterisk is allowed as wildcard => This is not supported
            // According to RFC 5538 a server can be specified and wildmat
            // syntax is allowed => Both options are not supported
            len = std::strlen(news);
            if(std::strlen(uri) >= len && !std::strncmp(uri, news, len))
            {
               // Accept empty <host> for <authority>: "news:///"
               if(!std::strncmp(&uri[len], "///", 3))
               {
                  std::memmove(&uri[len], &uri[len + (size_t) 3],
                               std::strlen(&uri[len + (size_t) 3])
                               + (size_t) 1);
               }
               if(std::strlen(uri) == len)
               {
                  SC("Do not use non-ASCII for the translation of this item")
                  fl_message_title(S("Error"));
                  fl_alert("%s", S("Invalid URI format"));
               }
               // Check for unsupported format with <authority>
               else if(std::strchr(&uri[len], (int) '/'))
               {
                  SC("Do not use non-ASCII for the translation of this item")
                  fl_message_title(S("Error"));
                  fl_alert("%s",
                           S("Server specification in URI not supported"));
               }
               // Check whether URI points to newsgroup or Message-ID
               // Attention: Overloaded and both prototypes different than in C!
               else if(!std::strchr(&uri[len], (int) '@'))
               {
                  // Select newsgroup
                  if(std::strchr(&uri[len], (int) '*')
                                 || std::strchr(&uri[len], (int) '?'))
                  {
                     SC("Do not use non-ASCII for the translation of this item")
                     fl_message_title(S("Error"));
                     fl_alert("%s", S("URI doesn't specify a single group"));
                  }
                  else
                  {
                     rv = enc_percent_decode(&uri[len], 1);
                     if(0 > rv)
                     {
                     SC("Do not use non-ASCII for the translation of this item")
                        fl_message_title(S("Error"));
                        fl_alert("%s", S("Invalid URI format"));
                     }
                     else
                     {
                        abort = true;
                        if(group_list)
                        {
                           for(gi = 0; gi < group_num; ++gi)
                           {
                              clen = std::strlen(&uri[len]);
                              if(std::strlen(group_list[gi].name) == clen)
                              {
                                 if(!strncmp(&uri[len], group_list[gi].name,
                                             clen))
                                 {
                                    if((std::size_t) INT_MAX >= ++gi)
                                    {
                                       groupList->deselect();
                                       groupList->select((int) gi);
                                       groupSelect(UI_CB_START, (int) gi);
                                       abort = false;
                                    }
                                    break;
                                 }
                              }
                           }
                        }
                     }
                     if(abort)
                     {
                        fl_message_title(S("Note"));
                        rv = fl_choice("%s", S("No"),
                                       S("Yes"), NULL,
                        // Don't wrap this line because of NLS macro!
            S("URI specifies a group that is not subscribed.\nSubscribe now?"));
                        if(rv)
                        {
                           rv = groupStateMerge();
                           if(!rv)
                           {
                              rv = core_subscribe_group(&uri[len]);
                              if(!rv)
                              {
                                 UI_STATUS(S("Group subscription stored."));
                                 // This check should silence code check tools
                                 // ('mainWindow' is always present)
                                 if(NULL != mainWindow)
                                 {
                                    // Import new group list
                                    mainWindow->groupListImport();
                                 }
                                 // Direct selection of the new group here is
                                 // not possible without core command queue.
                                 fl_message("%s",
                                 // Don't wrap this line because of NLS macro!
                              S("Click URI again after operation is complete"));
                              }
                           }
                        }
                     }
                  }
               }
               else
               {
                  // Select article that corresponds to Message-ID
                  rv = enc_percent_decode(&uri[len], 1);
                  if(0 > rv)
                  {
                     SC("Do not use non-ASCII for the translation of this item")
                     fl_message_title(S("Error"));
                     fl_alert("%s", S("Invalid URI format"));
                  }
                  else
                  {
                     clen = std::strlen(&uri[len]);
                     ti = searchSelectArticle(&uri[len], clen);
                     if(NULL == ti)
                     {
                        // Not found in article tree
                        if(main_debug)
                        {
                           printf("%s: %sTry to fetch article from URI\n",
                                  CFG_NAME, MAIN_ERR_PREFIX);
                        }
                        viewArticle(UI_CB_START, (const char*) &uri[len]);
                     }
                  }
               }
            }
            else
            {
               // Check for numeric value (Hyperlink to references list)
               clen = std::strlen(uri);
               if(std::strspn(uri, "0123456789") == clen)
               {
                  //fl_message("%s", "Hyperlink to references list");
                  if((std::size_t) INT_MAX >= clen && (std::size_t) 20 >= clen)
                  {
                     rv = enc_convert_ascii_to_anum(&ref_i, uri, (int) clen);
                     if(!rv)
                     {
                        p = currentArticleHE->header->refs[ref_i];
                        clen = std::strlen(p);
                        q = new char[clen];
                        std::strncpy(q, &p[1], clen - (std::size_t) 2);
                        q[clen - (std::size_t) 2] = 0;
                        viewArticle(UI_CB_START, q);
                        delete[] q;
                     }
                  }
               }
               else
               {
                  // Check for "mailto:" URI
                  len = std::strlen(mailto);
                  if(std::strlen(uri) >= len && !std::strncmp(uri, mailto, len))
                  {
                     if(std::strlen(uri) == len)
                     {
                     SC("Do not use non-ASCII for the translation of this item")
                        fl_message_title(S("Error"));
                        fl_alert("%s", S("Invalid URI format"));
                     }
                     else
                     {
                        // Yes => Use external e-mail handler
                        // Note:
                        // The 'mailto:' must be left in place to tell the
                        // handler that this is an URI and not an unencoded
                        // 'addr-spec'.
                        rv = ext_handler_email(uri, NULL, NULL);
                     }
                  }
                  else
                  {
                     // Check for "file:" URI
                     len = std::strlen(file);
                     if(std::strlen(uri) >= len
                        && !std::strncmp(uri, file, len))
                     {
                        if(std::strlen(uri) == len)
                        {
                     SC("Do not use non-ASCII for the translation of this item")
                           fl_message_title(S("Error"));
                           fl_alert("%s", S("Invalid URI format"));
                        }
                        else
                        {
                           // Yes => Store MIME entity to file
                           storeMIMEEntityToFile(uri,
                                               currentArticleHE->header->msgid);
                        }
                     }
                     else
                     {
                        // Use external URI handler for all other clickable
                        // links
                        rv = ext_handler_uri(uri, &invalid);
                        if(0 > rv && invalid)
                        {
                     SC("Do not use non-ASCII for the translation of this item")
                           fl_message_title(S("Error"));
                           fl_alert("%s", S("Invalid characters in URI"));
                        }
                        else if(rv)
                        {
                     SC("Do not use non-ASCII for the translation of this item")
                           fl_message_title(S("Error"));
                      fl_alert("%s", S("Starting external URI handler failed"));
                        }
                     }
                  }
               }
            }
         }
      }
   }
   std::free((void*) uri);
}


// =============================================================================
// Main window constructor
//
// \attention
// The X display is not set yet when the main window is constructed. Therefore
// anything done here is not allowed to trigger the connection to the X server.
//
// \param[in] label  Window title

MainWindow::MainWindow(const char*  label) :
   UI_WINDOW_CLASS(730, 395, label)
{
   const char*  enabled = S("Enabled");
   const char*  disabled = S("Disabled");
   const char*  available = S("Available");
   const char*  notavailable = S("Not available");
   Fl_Color  signature = conf_integer_check(CONF_COLOR_SIGNATURE, 0x00, 0xFF);
   Fl_Color  external = conf_integer_check(CONF_COLOR_EXTERNAL, 0x00, 0xFF);
   Fl_Color  hyperlink = conf_integer_check(CONF_COLOR_HYPERLINK, 0x00, 0xFF);
   Fl_Color  level1 = conf_integer_check(CONF_COLOR_LEVEL1, 0x00, 0xFF);
   Fl_Color  level2 = conf_integer_check(CONF_COLOR_LEVEL2, 0x00, 0xFF);
   Fl_Color  level3 = conf_integer_check(CONF_COLOR_LEVEL3, 0x00, 0xFF);
   Fl_Color  level4 = conf_integer_check(CONF_COLOR_LEVEL4, 0x00, 0xFF);
   const Fl_Color  styles_colors[UI_STYLES_LEN] =
   {
      FL_FOREGROUND_COLOR,             // Plain bold
      signature,                       // Signature
      external,                        // External citation
      hyperlink,                       // Hyperlink
      // -----------------------------------------------------------------------
      // Keep the following group continuous
      FL_FOREGROUND_COLOR,             // Content
      level1,                          // Thread citation level 1 (">  ")
      level2,                          // Thread citation level 2 ("> >  ")
      level3,                          // Thread citation level 3 ("> > >  ")
      level4                           // Thread citation level 4 ("> > > >  ")
   };
   Fl_Group*  mainGroup;
   Fl_Group*  statusGroup;
   Fl_Tree_Item*  ti;
   int  i;
   char*  p;

   mainState = STATE_READY;
   startup = true;
   busy = false;
   unsub = false;
   wrapMode = Fl_Text_Display::WRAP_NONE;
   hyperlinkStyle = 0;
   hyperlinkPosition = -1;
   rot13 = false;

   // Init group list, current group and current article
   group_num = 0;
   group_list = NULL;
   group_list_index = 0;
   group_old = 0;  // No group selected
   group_new = 0;
   subscribedGroups = NULL;
   currentGroup = NULL;
   currentArticle = NULL;
   currentStyle = NULL;
   lastArticleHE = NULL;
   currentArticleHE = NULL;
   currentLine = 0;

   // Init current search state
   p = new char[1];  p[0] = 0;  // Empty string
   currentSearchString = p;
   currentSearchPosition = 0;

   // Init progress bar state
   progress_skip_update = false;

   // Init MIME object pointer
   mimeData = NULL;

   // Init subescribecompose window pointer
   subscribeWindow = NULL;

   // Init compose window pointer and locking (for external editor)
   composeWindow = NULL;
   composeWindowLock = 0;

   // Install main window close callback
   callback(exit_cb, (void*) this);

   // Always dummy use both strings to avoid "unused" warnings from optimizer
   aboutString << enabled << disabled << available << notavailable;
   aboutString.str(std::string());
   aboutString.clear();

   // Create about message
   aboutString << CFG_NAME << " " << CFG_VERSION
               << " " << S("for") << " " << CFG_OS  << "\n"
#if defined(CFG_MODIFIED) && CFG_MODIFIED != 0
               // Don't use NLS for this string because of the license terms
               << "(This is a modified version!)" << "\n"
#endif  // CFG_MODIFIED
               // --------------------------------------------------------------
               << S("Unicode version") << ": " << UC_VERSION << "\n"
               // --------------------------------------------------------------
               << "NLS: "
#if CFG_USE_XSI && !CFG_NLS_DISABLE
               << S("Enabled") << "\n"
#else  // CFG_USE_XSI && !CFG_NLS_DISABLE
               << S("Disabled") << "\n"
#endif  // CFG_USE_XSI && !CFG_NLS_DISABLE
               // --------------------------------------------------------------
               "TLS: "
#if CFG_USE_TLS
               << S("Available") << "\n"
               << S("Required OpenSSL ABI") << ": "
#  if CFG_USE_LIBRESSL
               << "LibreSSL" << "\n"
#  else  // CFG_USE_LIBRESSL
#     if !CFG_USE_OPENSSL_API_3 && !CFG_USE_OPENSSL_API_1_1
               << "1.0" << "\n"
#     else  // !CFG_USE_OPENSSL_API_3 && !CFG_USE_OPENSSL_API_1_1
#        if !CFG_USE_OPENSSL_API_3
               << "1.1" << "\n"
#        else  // !CFG_USE_OPENSSL_API_3
               << "3" << "\n"
#        endif  // !CFG_USE_OPENSSL_API_3
#     endif  // !CFG_USE_OPENSSL_API_3 && !CFG_USE_OPENSSL_API_1_1
#  endif  // CFG_USE_LIBRESSL
#else  // CFG_USE_TLS
               << S("Not available") << "\n"
#endif  // CFG_USE_TLS
               // --------------------------------------------------------------
               << S("Required basexx ABI") << ": " << bxx0_version() << "\n"
               // --------------------------------------------------------------
               << S("Required ssiconv ABI") << ": " << ssic0_version() << "\n"
               // --------------------------------------------------------------
               << S("Required jpiconv ABI") << ": " << jpic0_version() << "\n"
               // --------------------------------------------------------------
               << S("Required uciconv ABI") << ": " << ucic0_version() << "\n"
               // --------------------------------------------------------------
               << S("Required FLTK ABI") << ": " << FL_MAJOR_VERSION << "."
               << FL_MINOR_VERSION
#ifdef FL_ABI_VERSION
               << "." << FL_ABI_VERSION % 100
#else  // FL_ABI_VERSION
               << "." << 0  // Not supported by FLTK 1.3.0
#endif  // FL_ABI_VERSION
               << "\n"
               // --------------------------------------------------------------
               << S("FLTK Double Buffering: ")
#if CFG_DB_DISABLE
               << S("Disabled") << "\n"
#else  // CFG_DB_DISABLE
               << S("Enabled") << "\n"
#endif  // CFG_DB_DISABLE
               // --------------------------------------------------------------
               << S("Compression: ")
#if CFG_CMPR_DISABLE
               << S("Disabled") << "\n"
#else  // CFG_CMPR_DISABLE
               << S("Available")
#  if CFG_USE_ZLIB
               << " (zlib)"
#  endif  // CFG_USE_ZLIB
               << "\n"
#endif  // CFG_CMPR_DISABLE
               // --------------------------------------------------------------
               << "Build: " << BDATE << std::flush;

   // Make main window resizable
   resizable(this);

   // Add widgets --------------------------------------------------------------
   begin();

   // Main group
   mainGroup = new Fl_Group(0, 0, 730, 395);
   mainGroup->begin();
   {
      // Menu bar
#if CFG_COCOA_SYS_MENUBAR
      menu = new Fl_Sys_Menu_Bar(0, 0, 730, 0);
#else  // CFG_COCOA_SYS_MENUBAR
      menu = new Fl_Menu_Bar(0, 0, 730, 30);
#endif  // CFG_COCOA_SYS_MENUBAR
      menu->selection_color(UI_COLOR_MENU_SELECTION);
#if defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
      // Recent FLTK 1.4 no longer shows selected entry with FL_FLAT_BOX
      menu->box(FL_THIN_UP_BOX);
#else  // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
      menu->box(FL_FLAT_BOX);
#endif  // defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
      // --------------------
      SC("")
      SC("This section is for the main window menubar and pull-down menus.")
      SC("The 2 spaces after every string are intended and must be preserved.")
      SC("The part before the slash is the menubar entry and must be unique.")
      SC("The character after & is the key to pull-down a menu with 'Alt-key'.")
      SC("You can assign the & to any character before the slash,")
      SC("provided again that it is unique in the menubar!")
      SC("The part behind the slash is the name of the pull-down menu entry,")
      SC("it must only be unique inside the corresponding pull-down menu.")
      SC("If the name contains a slash, a submenu is created.")
      SC("Note:")
      SC("It is not possible to localize the keyboard shortcuts for the")
      SC("pull-down menu entries. It was implemented this way for easier")
      SC("documentation and for the possibility to execute blind keyboard")
      SC("commands in the case of a misconfigured locale.")
      menu->add(S("&File/Save article  "), 0, asave_cb, (void*) this);
      menu->add(S("&File/Print article  "), 0, print_cb, (void*) this);
      menu->add(S("&File/Quit  "), "^q", exit_cb, (void*) this);
      // --------------------
      menu->add(S("&Edit/Server  "), 0, server_cb, (void*) this);
      menu->add(S("&Edit/Configuration  "), 0, config_cb, (void*) this);
      menu->add(S("&Edit/Identity  "), 0, identity_cb, (void*) this);
      // --------------------
      i = FL_MENU_TOGGLE;
      if(config[CONF_TVIEW].val.i)  { i |= FL_MENU_VALUE; }
      menu->add(S("&Group/Threaded view  "), "^t", thrv_cb, (void*) this, i);
      i = FL_MENU_TOGGLE;
      if(config[CONF_TVIEW].val.i)  { i |= FL_MENU_INACTIVE; }
      if(config[CONF_UTVIEW_AN].val.i)  { i |= FL_MENU_VALUE; }
      menu->add(S("&Group/Sort by article number  "), 0, uthrv_sort_cb,
                (void*) this, i);
      i = FL_MENU_TOGGLE;
      if(config[CONF_ONLYUR].val.i)  { i |= FL_MENU_VALUE; }
      menu->add(S("&Group/Show only unread articles  "), 0, onlyur_cb,
                (void*) this, i);
      menu->add(S("&Group/Subscribe  "), 0, gsubscribe_cb, (void*) this);
      menu->add(S("&Group/Unsubscribe  "), 0, gunsubscribe_cb, (void*) this);
      menu->add(S("&Group/Sort list  "), 0, gsort_cb, (void*) this);
      menu->add(S("&Group/Refresh list  "), "^r", grefresh_cb, (void*) this);
      menu->add(S("&Group/Next unread group  "), "^g", nug_cb, (void*) this);
      menu->add(S("&Group/Mark subthread read  "), 0, mssar_cb,
                (void*) this);
      menu->add(S("&Group/Mark all in group read  "), "^a", maar_cb,
                (void*) this);
      menu->add(S("&Group/Mark all groups read  "), 0, magar_cb,
                (void*) this);
      // --------------------
      menu->add(S("&Article/Search in article  "), "/", asearch_cb, (void*) this);
      menu->add(S("&Article/Post to newsgroup  "), "^p", compose_cb,
                (void*) this);
      menu->add(S("&Article/Followup to newsgroup  "), "^f", reply_cb,
                (void*) this);
      menu->add(S("&Article/Supersede in newsgroup  "), 0, supersede_cb,
                (void*) this);
      menu->add(S("&Article/Cancel in newsgroup  "), 0, cancel_cb,
                (void*) this);
      menu->add(S("&Article/Reply by e-mail  "), "^m", email_cb, (void*) this);
      menu->add(S("&Article/Wrap to width  "), "^w", wrap_cb, (void*) this);
      menu->add(S("&Article/ROT13  "),  "^o", rot13_cb, (void*) this);
      menu->add(S("&Article/Next unread article  "), "^n", nua_cb,
                (void*) this);
      menu->add(S("&Article/Previous read article  "), "^b", pra_cb,
                (void*) this);
      menu->add(S("&Article/View source  "), "^e", viewsrc_cb, (void*) this);
      menu->add(S("&Article/Toggle read unread  "), "^u", msau_cb,
                (void*) this);
      // --------------------
      i = FL_MENU_TOGGLE;
      if(main_debug)  { i |= FL_MENU_VALUE; }
      menu->add(S("&Tools/Protocol console  "), 0, console_cb, (void*) this);
      menu->add(S("&Tools/Search Message-ID  "), "^s", mid_search_cb,
                (void*) this);
      // --------------------
      menu->add(S("&Help/About  "), 0, about_cb, (void*) this);
      menu->add(S("&Help/Message of the day  "), 0, viewmotd_cb, (void*) this);
      menu->add(S("&Help/License  "), 0, license_cb, (void*) this);
      menu->add(S("&Help/Bug report  "), 0, bug_cb, (void*) this);
      SC("")
      // --------------------

      // Content group
#if CFG_COCOA_SYS_MENUBAR
      contentGroup = new Fl_Tile(0, 0, 730, 370);
#else  // CFG_COCOA_SYS_MENUBAR
      contentGroup = new Fl_Tile(0, 30, 730, 340);
#endif  // CFG_COCOA_SYS_MENUBAR
      contentGroup->begin();
      {
         // Group list
#if CFG_COCOA_SYS_MENUBAR
         groupList = new My_Multi_Browser(0, 0, 230, 370);
#else  // CFG_COCOA_SYS_MENUBAR
         groupList = new My_Multi_Browser(0, 30, 230, 340);
#endif  // CFG_COCOA_SYS_MENUBAR
         groupList->callback(gselect_cb, (void*) this);
#if CFG_COCOA_SYS_MENUBAR
         contentGroup2 = new Fl_Tile(230, 0, 500, 370);
#else  // CFG_COCOA_SYS_MENUBAR
         contentGroup2 = new Fl_Tile(230, 30, 500, 340);
#endif  // CFG_COCOA_SYS_MENUBAR
         contentGroup2->begin();
         {
            // Article tree
#if CFG_COCOA_SYS_MENUBAR
            articleTree = new My_Tree(230, 0, 500, 140);
#else  // CFG_COCOA_SYS_MENUBAR
            articleTree = new My_Tree(230, 30, 500, 140);
#endif  // CFG_COCOA_SYS_MENUBAR
            articleTree->showroot(0);
            articleTree->connectorstyle(FL_TREE_CONNECTOR_SOLID);
            articleTree->connectorcolor(FL_FOREGROUND_COLOR);
            // Explicitly set foreground color to FLTK default
            // (default is not the same as for the other widgets)
            articleTree->item_labelfgcolor(FL_FOREGROUND_COLOR);
            ti = articleTree->add(S("No articles"));
            if(NULL != ti)  { ti->deactivate(); }
            articleTree->callback(aselect_cb, (void*) this);
            // Article content window
#if CFG_COCOA_SYS_MENUBAR
            text = new My_Text_Display(230, 140, 500, 230);
#else  // CFG_COCOA_SYS_MENUBAR
            text = new My_Text_Display(230, 170, 500, 200);
#endif  // CFG_COCOA_SYS_MENUBAR
            text->callback(hyperlink_cb, (void*) this);
            text->textfont(FL_COURIER);
         }
         contentGroup2->end();
      }
      contentGroup->end();

      // Status group
      statusGroup = new Fl_Group(0, 370, 730, 25);
      statusGroup->begin();
      {
         // Progress bar
         progressBar = new Fl_Progress(0, 370, 100, 25);
         progressBar->color(FL_BACKGROUND_COLOR, UI_COLOR_PROGRESS_BAR);
         progressBar->minimum(0.0);
         progressBar->maximum(100.0);
         progressBar->value(0.0);
         progressBar->label("");
         // Status bar
         statusBar = new Fl_Box(100, 370, 730, 25);
         statusBar->box(FL_DOWN_BOX);
         statusBar->align(FL_ALIGN_INSIDE | FL_ALIGN_LEFT);
         statusBar->label("");
      }
      statusGroup->end();
      statusGroup->resizable(statusBar);
   }
   mainGroup->end();
   mainGroup->resizable(contentGroup);

   end();
   // --------------------------------------------------------------------------

   // Allocate and init styles
   styles_len = UI_STYLES_LEN;
   styles = new Fl_Text_Display::Style_Table_Entry[styles_len];
   styles[0].color = FL_FOREGROUND_COLOR;
   styles[0].font = FL_COURIER_BOLD;
   styles[0].size = text->textsize();
   styles[0].attr = 0;
   for(i = 1; i < styles_len; ++i)
   {
      styles[i].color = styles_colors[i];
      styles[i].font = text->textfont();
      styles[i].size = text->textsize();
      styles[i].attr = 0;
   }
}


// =============================================================================
// Main window destructor
//
// \attention
// The FLTK documentation specifies that 'highlight_data()' can remove a style
// buffer, but it's unclear how this should work. Passing 'NULL' is not allowed
// because the pointer to the new style buffer is always dereferenced.
// For 'buffer()' it is not explicitly allowed to pass 'NULL' in the
// documentation too. Even if it works this may change in future releases.
// As a workaround we assign a dummy text buffer before destroying the attached
// one.

MainWindow::~MainWindow(void)
{
   if(NULL != subscribedGroups)
   {
      core_destroy_subscribed_group_info(&subscribedGroups);
   }
   if(NULL != mimeData)  { delete mimeData; }
   delete[] currentSearchString;
   core_free(currentGroup);
   core_destroy_subscribed_group_states(&group_num, &group_list);
   // Detach highlight data from 'text' before destroying it
   text->highlight_data(dummyTb, NULL, 0, 'A', NULL, NULL);
   if(currentStyle)  { delete currentStyle; }
   delete[] styles;
   // Detach current article from 'text' before destroying it
   text->buffer(dummyTb);
   if(currentArticle)  { delete currentArticle; }
}


// =============================================================================
// Server configuration window UI driving

int  ServerCfgWindow::process(void)
{
   // Drive UI until user press OK or Cancel button
   while(!finished)  { Fl::wait(); }

   // Copy data back to server configuration object
   sconf->serverReplace(scfgHostname->value());
   sconf->serviceReplace(scfgService->value());
   if(scfgTlsStrong->value())  { sconf->enc = UI_ENC_STRONG; }
   else if(scfgTlsWeak->value())  { sconf->enc = UI_ENC_WEAK; }
   else { sconf->enc = UI_ENC_NONE; }
   if(scfgAuthUser->value())  { sconf->auth = UI_AUTH_USER; }
   else { sconf->auth = UI_AUTH_NONE; }

   return(finished);
}


// =============================================================================
// Server configuration window constructor

ServerCfgWindow::ServerCfgWindow(ServerConfig*  sc, const char*  label) :
   UI_WINDOW_CLASS(700, 395, label)
{
   Fl_Group*  gp;
   Fl_Box*  bp;
   Fl_Button*  p;
   int  y, w, h, gy, gh, bw;
   int  w1, w2, h1, h2;
   std::ostringstream  serverString;
   std::ostringstream  serviceString;
   std::ostringstream  tlsString;
   Fl_Group*  grpTls;

   // Configure server window
   sconf = sc;
   finished = 0;
   copy_label(label);
   set_modal();
   callback(cancel_cb, (void*) this);

   // Prepare label for server hostname input field
   serverString << S("NNTP server hostname")
                << " (" << S("or IP address") << ")" << std::flush;
   // Prepare label for server service input field
   serviceString << S("Service name")
                 << " (" << S("or TCP port") << ")" << std::flush;
   tlsString << "Transport Layer Security (TLS)"
             << " " << S("and server to client authentication") << std::flush;

   // Attention:
   //
   //    const char*  server = serverString.str().c_str()
   //
   // creates 'server' with undefined value because the compiler is
   // allowed to free the temporary string object returned by 'str()'
   // immediately after the assignment!
   // Assigning names to temporary string objects forces them to stay in
   // memory as long as their names go out of scope (this is what we
   // need).
   const std::string&  srv_s = serverString.str();
   const std::string&  svc_s = serviceString.str();
   const std::string&  tls_s = tlsString.str();

   // Copy C strings
   hostname = new char[std::strlen(srv_s.c_str()) + (std::size_t) 1];
   std::strcpy(hostname, srv_s.c_str());
   service = new char[std::strlen(svc_s.c_str()) + (std::size_t) 1];
   std::strcpy(service, svc_s.c_str());
   tls_headline = new char[std::strlen(tls_s.c_str()) + (std::size_t) 1];
   std::strcpy(tls_headline, tls_s.c_str());

   // Create widget group
   scfgGroup = new Fl_Group(0, 0, 700, 395);
   scfgGroup->begin();
   {
      // Calculate required widths and heights
      gui_set_default_font();
      fl_measure(S("Cancel"), w1 = 0, h1 = 0);
      fl_measure(S("OK"), w2 = 0, h2 = 0);
      if(w1 > w2)  { w = w1; }  else  { w = w2; }
      if(h1 > h2)  { h = h1; }  else  { h = h2; }
      w += 30;
      h += 10;
      gh = h + 10;
      gy = 395 - gh;
      // Add server hostname field
      scfgHostname = new Fl_Input(15, 35, 450, 30, hostname);
      scfgHostname->align(FL_ALIGN_TOP_LEFT);
      scfgHostname->value(sc->server);
      // Add server TCP port field
      scfgService = new Fl_Input(480, 35, 205, 30, service);
      scfgService->align(FL_ALIGN_TOP_LEFT);
      scfgService->value(sc->service);
      // Add TLS radio buttons
      grpTls = new Fl_Group(15, 100, 670, 120, tls_headline);
      grpTls->begin();
      {
         scfgTlsOff = new Fl_Radio_Round_Button(30, 115, 640, 30,
            S("Disabled"));
         scfgTlsOff->selection_color(UI_COLOR_RADIO_BUTTON);
         scfgTlsOff->callback(enc_off_cb, (void*) this);
         scfgTlsStrong = new Fl_Radio_Round_Button(30, 145, 640, 30,
            S("Enabled with strong encryption and forward secrecy"));
         scfgTlsStrong->selection_color(UI_COLOR_RADIO_BUTTON);
         scfgTlsStrong->callback(enc_on_cb, (void*) this);
         scfgTlsWeak = new Fl_Radio_Round_Button(30, 175, 640, 30,
            S("Enabled in compatibility mode offering weak cipher suites"));
         scfgTlsWeak->selection_color(UI_COLOR_RADIO_BUTTON);
         scfgTlsWeak->callback(enc_on_cb, (void*) this);
      }
      grpTls->end();
      grpTls->align(FL_ALIGN_TOP_LEFT);
      grpTls->box(FL_EMBOSSED_BOX);
#if !CFG_USE_TLS
      grpTls->deactivate();
      sc->enc = UI_ENC_NONE;
#endif  // CFG_USE_TLS
      switch(sc->enc)
      {
         case UI_ENC_STRONG:  { scfgTlsStrong->set();  break; }
         case UI_ENC_WEAK:  { scfgTlsWeak->set();  break; }
         default:  { scfgTlsOff->set();  break; }
      }
      // Add authentication radio buttons
      grpAuth = new Fl_Group(15, 255, 670, 90,
         S("Client to server authentication"));
      grpAuth->begin();
      {
         scfgAuthOff = new Fl_Radio_Round_Button(30, 270, 640, 30,
            S("Disabled"));
         scfgAuthOff->selection_color(UI_COLOR_RADIO_BUTTON);
         scfgAuthUser = new Fl_Radio_Round_Button(30, 300, 640, 30,
            S("AUTHINFO USER/PASS as defined in RFC 4643"));
         scfgAuthUser->selection_color(UI_COLOR_RADIO_BUTTON);
      }
      grpAuth->end();
      grpAuth->align(FL_ALIGN_TOP_LEFT);
      grpAuth->box(FL_EMBOSSED_BOX);
#if !CFG_NNTP_AUTH_UNENCRYPTED
      if(UI_ENC_NONE == sc->enc)
      {
         scfgAuthOff->set();
         grpAuth->deactivate();
      }
      else
#endif
      {
         switch(sc->auth)
         {
            case UI_AUTH_USER:  { scfgAuthUser->set();  break; }
            default:  { scfgAuthOff->set();  break; }
         }
      }
      // Add button bar at bottom
      gp = new Fl_Group(0, gy, 700, gh);
      gp->begin();
      {
         bw = w + 30;
         y = gy + 5;
         new Fl_Box(0, gy, bw, gh);
         p = new Fl_Button(15, y, w, h, S("OK"));
         p->callback(ok_cb, (void*) this);
         new Fl_Box(500 - bw, gy, bw, gh);
         p = new Fl_Button(700 - bw + 15, y, w, h, S("Cancel"));
         p->callback(cancel_cb, (void*) this);
         // Resizable space between buttons
         bp = new Fl_Box(bw, gy, 700 - (2 * bw), gh);
      }
      gp->end();
      gp->resizable(bp);
   }
   scfgGroup->end();
   resizable(scfgGroup);
   // Set fixed window size
   size_range(700, 395, 700, 395);
#if !CFG_DB_DISABLE
   Fl::visual(FL_DOUBLE | FL_INDEX);
#endif  // CFG_DB_DISABLE
   show();
}


// =============================================================================
// Server configuration window destructor

ServerCfgWindow::~ServerCfgWindow(void)
{
   delete[]  hostname;
   delete[]  service;
   delete[]  tls_headline;
}


// =============================================================================
// Identity configuration window callback
//
// Only store the Unicode data here, the encoding is done elsewhere.

void  IdentityCfgWindow::ok_cb_i(void)
{
   std::size_t  len1;
   std::size_t  len2;
   char*  buf;

   // Store "From" header in Unicode format (without MIME encoding)
   len1 = std::strlen(fromName->value());
   len2 = std::strlen(fromEmail->value());
   if(UI_HDR_BUFSIZE <= len1 || UI_HDR_BUFSIZE <= len2)
   {
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s", S("From header field element too long"));
   }
   buf = new char[len1 + len2 + (std::size_t) 4];
   buf[0] = 0;
   if(len2)
   {
      if(len1)  { std::strcat(buf, fromName->value()); }
      std::strcat(buf, " <");
      std::strcat(buf, fromEmail->value());
      std::strcat(buf, ">");
   }
   conf_string_replace(&config[CONF_FROM], buf);
   delete[]  buf;

   // Store body for "Reply-To" header field
   len1 = std::strlen(replytoName->value());
   len2 = std::strlen(replytoEmail->value());
   if(UI_HDR_BUFSIZE <= len1 || UI_HDR_BUFSIZE <= len2)
   {
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s", S("Reply-To header field element too long"));
   }
   buf = new char[len1 + len2 + (std::size_t) 4];
   buf[0] = 0;
   if(len2)
   {
      if(len1)  { std::strcat(buf, replytoName->value()); }
      std::strcat(buf, " <");
      std::strcat(buf, replytoEmail->value());
      std::strcat(buf, ">");
   }
   if(std::strcmp(buf, config[CONF_FROM].val.s))
   {
      conf_string_replace(&config[CONF_REPLYTO], buf);
   }
   else  { conf_string_replace(&config[CONF_REPLYTO], ""); }
   delete[]  buf;

   // Destroy configuration window
   Fl::delete_widget(this);
}


// =============================================================================
// Identity configuration window constructor

IdentityCfgWindow::IdentityCfgWindow(const char*  label) :
   UI_WINDOW_CLASS(700, 200, label)
{
   char  from_name[UI_HDR_BUFSIZE + (std::size_t) 1];
   char  from_email[UI_HDR_BUFSIZE + (std::size_t) 1];
   char  replyto_name[UI_HDR_BUFSIZE + (std::size_t) 1];
   char  replyto_email[UI_HDR_BUFSIZE + (std::size_t) 1];
   std::size_t  i;
   Fl_Group*  gp1;
   Fl_Group*  gp2;
   Fl_Box*  bp;
   Fl_Button*  p;
   int  y, w, h, gy, gh, bw;
   int  w1, w2, h1, h2;

   // Configure identity window
   copy_label(label);
   set_modal();
   callback(cancel_cb, (void*) this);

   // Split current identity configuration into names and e-mail addresses
   std::strncpy(from_name, config[CONF_FROM].val.s, UI_HDR_BUFSIZE);
   from_name[UI_HDR_BUFSIZE] = 0;
   from_email[0] = 0;
   if(std::strlen(from_name))
   {
      i = 0;
      while(from_name[i])
      {
         if('<' == from_name[i])
         {
            // Attention: Overloaded and both prototypes different than in C!
            if(NULL == std::strchr(&from_name[i + (std::size_t) 1], (int) '<'))
            {
               // Remaining data doesn't contain another opening angle bracket
               if(!i || ((std::size_t) 1 == i && ' ' == from_name[0]))
               {
                  from_name[0] = 0;
               }
               else  { from_name[i - (std::size_t) 1] = 0; }
               std::strncpy(from_email, &from_name[++i], UI_HDR_BUFSIZE);
               i = std::strlen(from_email) - (std::size_t) 1;
               if('>' != from_email[i])  { from_email[0] = 0; }
               else  { from_email[i] = 0; }
               break;
            }
         }
         ++i;
      }
   }
   std::strncpy(replyto_name, config[CONF_REPLYTO].val.s, UI_HDR_BUFSIZE);
   replyto_name[UI_HDR_BUFSIZE] = 0;
   replyto_email[0] = 0;
   if(std::strlen(replyto_name))
   {
      i = 0;
      while(replyto_name[i])
      {
         if('<' == replyto_name[i])
         {
            if(!i || ((std::size_t) 1 == i && ' ' == replyto_name[0]))
            {
               replyto_name[0] = 0;
            }
            else  { replyto_name[i - (std::size_t) 1] = 0; }
            std::strncpy(replyto_email, &replyto_name[++i], UI_HDR_BUFSIZE);
            i = std::strlen(replyto_email) - (std::size_t) 1;
            if('>' != replyto_email[i])  { replyto_email[0] = 0; }
            else  { replyto_email[i] = 0; }
            break;
         }
         else  { ++i; }
      }
   }

   // Create widget group
   cfgGroup = new Fl_Group(0, 0, 700, 200);
   cfgGroup->begin();
   {
      // Calculate required widths and heights
      gui_set_default_font();
      fl_measure(S("Cancel"), w1 = 0, h1 = 0);
      fl_measure(S("OK"), w2 = 0, h2 = 0);
      if(w1 > w2)  { w = w1; }  else  { w = w2; }
      if(h1 > h2)  { h = h1; }  else  { h = h2; }
      w += 30;
      h += 10;
      gh = h + 10;
      gy = 200 - gh;

      gp1 = new Fl_Group(0, 0, 700, gy);
      gp1->begin();
      {
         // Add "From" Name field
         fromName = new Fl_Input(15, 35, 325, 30, "From (Name):");
         fromName->align(FL_ALIGN_TOP_LEFT);
         fromName->value(from_name);
         // Add "From" e-mail field
         fromEmail = new Fl_Input(355, 35, 325, 30, "From (e-mail):");
         fromEmail->align(FL_ALIGN_TOP_LEFT);
         fromEmail->value(from_email);
         // Add "Reply-To" Name field
         replytoName = new Fl_Input(15, 100, 325, 30, "Reply-To (Name):");
         replytoName->align(FL_ALIGN_TOP_LEFT);
         replytoName->value(replyto_name);
         // Add "Reply-To" e-mail field
         replytoEmail = new Fl_Input(355, 100, 325, 30, "Reply-To (e-mail):");
         replytoEmail->align(FL_ALIGN_TOP_LEFT);
         replytoEmail->value(replyto_email);
         // Resizable space below input fields
         bp = new Fl_Box(0, 150, 700, gy - 150);
         //bp->box(FL_DOWN_BOX);
      }
      gp1->end();
      gp1->resizable(bp);
      // Add button bar at bottom
      gp2 = new Fl_Group(0, gy, 700, gh);
      gp2->begin();
      {
         bw = w + 30;
         y = gy + 5;
         new Fl_Box(0, gy, bw, gh);
         p = new Fl_Button(15, y, w, h, S("OK"));
         p->callback(ok_cb, (void*) this);
         new Fl_Box(500 - bw, gy, bw, gh);
         p = new Fl_Button(700 - bw + 15, y, w, h, S("Cancel"));
         p->callback(cancel_cb, (void*) this);
         // Resizable space between buttons
         bp = new Fl_Box(bw, gy, 700 - (2 * bw), gh);
      }
      gp2->end();
      gp2->resizable(bp);
   }
   cfgGroup->end();
   cfgGroup->resizable(gp1);
   resizable(cfgGroup);
   // Set minimum window size
   size_range(700, 150 + gh, 0, 0);
#if !CFG_DB_DISABLE
   Fl::visual(FL_DOUBLE | FL_INDEX);
#endif  // CFG_DB_DISABLE
   show();
}


// =============================================================================
// Identity configuration window destructor

IdentityCfgWindow::~IdentityCfgWindow(void)
{
}


// =============================================================================
// Miscellaneous configuration window callback

void  MiscCfgWindow::ok_cb_i(void)
{
   const char*  is;
   const unsigned int  cac_limit = (unsigned int) UI_CAC_MAX;
   unsigned int  i;
   int  error = 1;

   // Update CAC configuration
   is = cacField->value();
   if(NULL != is && 1 == std::sscanf(is, "%u", &i))
   {
      if(cac_limit < i)  { i = cac_limit; }
      config[CONF_CAC].val.i = (int) i;
      error = 0;
   }
   else
   {
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s", S("Invalid value for CAC"));
   }

   // Update checkbox settings
   if(!error)
   {
      if(cmprEnable->value())  { config[CONF_COMPRESSION].val.i = 1; }
      else  { config[CONF_COMPRESSION].val.i = 0; }
      if(localTime->value())  { config[CONF_TS_LTIME].val.i = 1; }
      else  { config[CONF_TS_LTIME].val.i = 0; }
      if(uagentEnable->value())  { config[CONF_ENABLE_UAGENT].val.i = 1; }
      else  { config[CONF_ENABLE_UAGENT].val.i = 0; }

      if(qsSpace->value())  { config[CONF_QUOTESTYLE].val.i = 1; }
      else  { config[CONF_QUOTESTYLE].val.i = 0; }
      if(qsUnify->value())  { config[CONF_QUOTEUNIFY].val.i = 1; }
      else  { config[CONF_QUOTEUNIFY].val.i = 0; }
   }

   // Destroy misc configuration window if no error was detected
   if(!error)  { Fl::delete_widget(this); }
}


// =============================================================================
// Miscellaneous configuration window constructor

MiscCfgWindow::MiscCfgWindow(const char*  label) :
   UI_WINDOW_CLASS(400, 235, label)
{
   Fl_Group*  gp1;
   Fl_Group*  gp2;
   Fl_Box*  bp;
   Fl_Button*  p;
   int  th = 30;  // Tab hight (including gap)
   int  tg = 5;  // Gap between tabs and cards
   int  y, w, h, gy, gh, bw;
   int  w1, w2, h1, h2;
   std::ostringstream  ss;
   // Label strings
   SC("Preserve the spaces at beginning and end")
   const char*  label_tab_cac = S("  Misc  ");
   const char*  label_tab_qs = S("  Quote style  ");
   SC("Preserve the colon")
   const char*  label_cac = S("Clamp article count threshold:");

   // Configure config window
   copy_label(label);
   set_modal();
   callback(cancel_cb, (void*) this);

   // Create widget group
   gp1 = new Fl_Group(0, 0, 400, 235);
   gp1->begin();
   {
      // Calculate required widths and heights
      gui_set_default_font();
      fl_measure(S("Cancel"), w1 = 0, h1 = 0);
      fl_measure(S("OK"), w2 = 0, h2 = 0);
      if(w1 > w2)  { w = w1; }  else  { w = w2; }
      if(h1 > h2)  { h = h1; }  else  { h = h2; }
      w += 30;
      h += 10;
      gh = h + 10;
      gy = 235 - gh;

      cfgTabs = new Fl_Tabs(0, 0, 400, gy);
      cfgTabs->begin();
      {
         cacGroup = new Fl_Group(0, th - tg, 400, 235 - th + tg, label_tab_cac);
         cacGroup->begin();
         {
            cacField = new Fl_Input(15, 55, 400 - 30, 30, label_cac);
            cacField->align(FL_ALIGN_TOP_LEFT);
            // Note: We cannot use 'posix_snprintf()' here
            ss << config[CONF_CAC].val.i << std::flush;
            const std::string&  s = ss.str();
            cacField->value(s.c_str());
            cmprEnable = new Fl_Check_Button(15, 90, 400 - 30, 30,
                                       S("Negotiate compression if available"));
            cmprEnable->tooltip(
            SC("This is the tooltip for the compression negotiation checkbox")
               S("Not recommended when confidential newsgroups are accessed")
            );
            cmprEnable->value(config[CONF_COMPRESSION].val.i);
#if CFG_CMPR_DISABLE
            cmprEnable->deactivate();
#endif  // CFG_CMPR_DISABLE
            localTime = new Fl_Check_Button(15, 120, 400 - 30, 30,
                                      S("Use localtime for Date header field"));
            localTime->tooltip(
               SC("This is the tooltip for the use localtime checkbox")
               S("Using localtime is recommended by RFC 5322")
            );
            localTime->value(config[CONF_TS_LTIME].val.i);
            uagentEnable = new Fl_Check_Button(15, 150, 400 - 30, 30,
                                              S("Add User-Agent header field"));
            uagentEnable->value(config[CONF_ENABLE_UAGENT].val.i);
            // Resizable space below input fields
            bp = new Fl_Box(0, 180, 400, gy - 150);
            //bp->box(FL_DOWN_BOX);
         }
         cacGroup->end();
         cacGroup->resizable(bp);

         // --------------------------------------------------------------------

         qsGroup = new Fl_Group(0, th - tg, 400, 235 - th + tg, label_tab_qs);
         qsGroup->begin();
         {
            qsSpace = new Fl_Check_Button(15, 45, 400 - 30, 30,
                                          S("Space after quote marks"));
            qsSpace->value(config[CONF_QUOTESTYLE].val.i);
            qsUnify = new Fl_Check_Button(15, 75, 400 - 30, 30,
                                          S("Unify quote marks for follow-up"));
            qsUnify->value(config[CONF_QUOTEUNIFY].val.i);
            // Resizable space below input fields
            bp = new Fl_Box(0, 150, 400, gy - 150);
            //bp->box(FL_DOWN_BOX);
         }
         qsGroup->end();
         qsGroup->resizable(bp);
      }
      cfgTabs->end();
      cfgTabs->resizable(cacGroup);
      // Add button bar at bottom
      gp2 = new Fl_Group(0, gy, 400, gh);
      gp2->begin();
      {
         bw = w + 30;
         y = gy + 5;
         new Fl_Box(0, gy, bw, gh);
         p = new Fl_Button(15, y, w, h, S("OK"));
         p->callback(ok_cb, (void*) this);
         new Fl_Box(500 - bw, gy, bw, gh);
         p = new Fl_Button(400 - bw + 15, y, w, h, S("Cancel"));
         p->callback(cancel_cb, (void*) this);
         // Resizable space between buttons
         bp = new Fl_Box(bw, gy, 400 - (2 * bw), gh);
      }
      gp2->end();
      gp2->resizable(bp);
   }
   gp1->end();
   gp1->resizable(cfgTabs);
   resizable(gp1);
   // Set minimum window size
   size_range(400, 235, 0, 0);
#if !CFG_DB_DISABLE
   Fl::visual(FL_DOUBLE | FL_INDEX);
#endif  // CFG_DB_DISABLE
   show();
}


// =============================================================================
// Miscellaneous configuration window destructor

MiscCfgWindow::~MiscCfgWindow(void)
{
}


// =============================================================================
// Search window callback

void  SearchWindow::ok_cb_i(void)
{
   const char*  ss;
   const char*  ss_nfc;
   int  len;
   std::size_t  tmp;
   char*  p;

   // Check search string
   ss = searchField->value();
   // Convert search string to Unicode Normalization Form C
   ss_nfc = enc_convert_to_utf8_nfc(ENC_CS_UTF_8, ss);
   if(NULL == ss_nfc)
   {
      UI_STATUS(S("Search failed."));
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s", S("Processing Unicode data in search string failed"));
   }
   else
   {
      if(std::strcmp(*currentSearchString, ss_nfc))
      {
         // Search string has changed
         tmp = std::strlen(ss_nfc);
         if((std::size_t) INT_MAX <= tmp)  { len = INT_MAX - 1; }
         else { len = (int) tmp; }
         // Replace current search string
         delete[] *currentSearchString;
         p = new char[len + 1];
         std::strncpy(p, ss_nfc, (size_t) len);
         p[len] = 0;
         *currentSearchString = p;
      }
      // Release memory for normalized string (if allocated)
      if(ss != ss_nfc)  { enc_free((void*) ss_nfc); }

      // Check for case-insensitive search request
      config[CONF_SEARCH_CASE_IS].val.i = cisEnable->value();
   }
}


// =============================================================================
// Search window constructor

SearchWindow::SearchWindow(const char*  label, const char**  oldSearchString) :
   UI_WINDOW_CLASS(400, 165, label)
{
   Fl_Group*  gp;  // Toplevel group in window
   Fl_Group*  searchGroup;
   Fl_Group*  searchGroup2;
   Fl_Group*  buttonGroup;
   Fl_Box*  bp;
   Fl_Button*  p;
   int  y, w, h, gy, gh, bw;
   int  w1, w2, h1, h2;
   std::ostringstream  ss;
   // Label strings
   SC("Preserve the colon")
   const char*  label_search = S("Search for:");

   // Init finished flag
   finished = 0;

   // Init pointer to current search string
   currentSearchString = oldSearchString;

   // Configure config window
   copy_label(label);
   set_modal();
   callback(cancel_cb, (void*) this);

   // Create widget group
   gp = new Fl_Group(0, 0, 400, 165);
   gp->begin();
   {
      // Calculate required widths and heights
      gui_set_default_font();
      fl_measure(S("Cancel"), w1 = 0, h1 = 0);
      fl_measure(S("OK"), w2 = 0, h2 = 0);
      if(w1 > w2)  { w = w1; }  else  { w = w2; }
      if(h1 > h2)  { h = h1; }  else  { h = h2; }
      w += 30;
      h += 10;
      gh = h + 10;    // Height of bottom button bar
      gy = 165 - gh;  // Height of top group
      searchGroup = new Fl_Group(0, 0, 400, 165 - gh);
      searchGroup->begin();
      {
         // Wrap a separate group around the input field
         searchGroup2 = new Fl_Group(0, 0, 400, 80);
         searchGroup2->begin();
         {
            // Input field in inner group
            searchField = new Fl_Input(15, 35, 400 - 30, 30, label_search);
            searchField->align(FL_ALIGN_TOP_LEFT);
            // Note: We cannot use 'posix_snprintf()' here
            ss << *currentSearchString << std::flush;
            const std::string&  s = ss.str();
            searchField->value(s.c_str());
            searchField
#if defined FL_ABI_VERSION && 10400 <= FL_ABI_VERSION
            // Requires FLTK 1.4.0
            ->insert_position
#else  // defined FL_ABI_VERSION && 10303 <= FL_ABI_VERSION
            ->position
#endif  // defined FL_ABI_VERSION && 10303 <= FL_ABI_VERSION
                             (searchField->size(), 0);
            searchField->take_focus();
         }
         searchGroup2->end();
         // Check button in outer group
         cisEnable = new Fl_Check_Button(15, 80, 400 - 30, 30,
                                         S("Search case-insensitive"));
         cisEnable->value(config[CONF_SEARCH_CASE_IS].val.i);
         cisEnable->clear_visible_focus();
         // Resizable space below input fields
         bp = new Fl_Box(0, 110, 400, gy - 110);
         //bp->box(FL_DOWN_BOX);
         //bp->color(FL_GREEN);
      }
      searchGroup->end();
      searchGroup->resizable(bp);
      // Add button bar at bottom
      buttonGroup = new Fl_Group(0, gy, 400, gh);
      buttonGroup->begin();
      {
         bw = w + 30;
         y = gy + 5;
         new Fl_Box(0, gy, bw, gh);
         p = new Fl_Button(15, y, w, h, S("OK"));
         p->callback(ok_cb, (void*) this);
         p->shortcut(FL_Enter);
         p->clear_visible_focus();
         new Fl_Box(400 - bw, gy, bw, gh);
         p = new Fl_Button(400 - bw + 15, y, w, h, S("Cancel"));
         p->callback(cancel_cb, (void*) this);
         p->shortcut(FL_Escape);
         p->clear_visible_focus();
         // Resizable space between buttons
         bp = new Fl_Box(bw, gy, 400 - (2 * bw), gh);
         //bp->box(FL_DOWN_BOX);
         //bp->color(FL_YELLOW);
      }
      buttonGroup->end();
      buttonGroup->resizable(bp);
   }
   gp->end();
   gp->resizable(searchGroup);
   resizable(gp);
   // Set minimum window size
   size_range(400, 165, 0, 0);
#if !CFG_DB_DISABLE
   Fl::visual(FL_DOUBLE | FL_INDEX);
#endif  // CFG_DB_DISABLE
   show();
}


// =============================================================================
// Search window destructor

SearchWindow::~SearchWindow(void)
{
}


// =============================================================================
// MIME content element decoder
//
// All the headerfields currently used are mandatory and the CORE module ensures
// that strings are not NULL.
// This must be checked here if optional header fields are used in the future!
//
// The caller is responsible to enc_free() the memory allocated for the result.

const char*  MIMEContent::createMessageHeader(const char*  message,
                                              std::size_t  len)
{
   const char*  res = NULL;
   core_article_header*  hdr = NULL;
   std::size_t  unused;
   Fl_Text_Buffer  tb;
   char  date[20];
   std::size_t  i = 0;
   const char*  p;

   // Split and parse header of message
   p = core_entity_parser(message, len, &hdr, &unused);
   if(NULL == p)
   {
      // Invalid message
      tb.append("[Invalid MIME message/rfc822 content]");
      tb.append("\n");
   }
   else
   {
      // From header field
      tb.append(S("From"));
      tb.append(": ");
      tb.append(hdr->from);
      tb.append("\n");

      // Subject header field
      tb.append(S("Subject"));
      tb.append(": ");
      tb.append(hdr->subject);
      tb.append("\n");

      // Date header field (a zero timestamp is treated as missing header field)
      tb.append(S("Date"));
      tb.append(": ");
      if(!hdr->date || enc_convert_posix_to_iso8601(date, hdr->date))
      {
         // No NLS support here to stay consistent with CORE
         tb.append("[Missing or invalid header field]");
      }
      else
      {
         tb.append(date);
      }
      tb.append("\n");

      // Message-ID header field
      tb.append(S("Message-ID"));
      tb.append(": ");
      if(!hdr->msgid[0])
      {
         // No NLS support here to stay consistent with CORE
         tb.append("[Missing or invalid header field]");
      }
      else
      {
         tb.append(hdr->msgid);
      }
      tb.append("\n");

      // Newsgroups header field
      tb.append(S("Newsgroups"));
      tb.append(": ");
      while(hdr->groups[i])
      {
         if(!i && !hdr->groups[0][0])
         {
            // No NLS support here to stay consistent with CORE
            tb.append("[Missing or invalid header field]");
            break;
         }
         else
         {
            if(i)  { tb.append(","); }
            tb.append(hdr->groups[i++]);
         }
      }
      tb.append("\n");
   }

   // Copy result
   p = tb.text();
   res = enc_convert_posix_to_canonical(p);

   std::free((void*) p);
   core_destroy_entity_header(&hdr);

   return(res);
}


// =============================================================================
// MIME content element decoder
//
// \attention
// This method must be reentrant (calls itself to recursively decode nested
// multipart entities

MIMEContent::MIMEContentListElement*
MIMEContent::decodeElement(const char*  p, std::size_t  len, char*  boundary,
                           bool  alt, bool  digest, std::size_t*  count)
{
   MIMEContentListElement*  res = NULL;  // Result list
   MIMEContentListElement*  cle = NULL;  // Current list element(s)
   MIMEContentListElement*  ble = NULL;  // Buffered list element(s)
                                         // for multipart/alternative
   MIMEContentListElement*  mle = NULL;  // Encapsulated List element(s)
                                         // for message/rfc822
   MIMEContentListElement*  lle = NULL;  // Last list element
   std::size_t  ae;  // Additional elements
   std::size_t  bae = 0;  // Buffered additional elements
   std::size_t  num = 0;
   enc_mime_mpe*  mpe = NULL;  // Multipart entities
   enc_mime_cte  cte;
   enc_mime_ct  ct;
   bool  nested_multi;
   char  nested_boundary[ENC_BO_BUFLEN];
   bool  nested_alt;
   bool  nested_digest;
   bool  message_rfc822;
   core_article_header*  e_h = NULL;
   std::size_t  e_len;
   const char*  q;
   std::size_t  i;
   MIMEContentListElement*  tmp;

   // Split multipart message into entities
   if(main_debug)
   {
      if(!std::strlen(boundary))
      {
         printf("%s: %svvv Extract MIME encapsulated message vvv\n",
                CFG_NAME, MAIN_ERR_PREFIX);
      }
      else
      {
         printf("%s: %s--- Split MIME multipart entity ---\n",
                CFG_NAME, MAIN_ERR_PREFIX);
      }
      printf("%s: %sSize of body: %lu octets\n",
              CFG_NAME, MAIN_ERR_PREFIX, (unsigned long int) len);
   }
   if(!std::strlen(boundary))
   {
      num = enc_mime_message(p, len, &mpe);
   }
   else
   {
      num = enc_mime_multipart(p, boundary, &mpe);
   }
   for(i = 0; i < num; ++i)
   {
      cte = ENC_CTE_BIN;
      nested_multi = false;
      nested_alt = false;
      nested_digest = false;
      message_rfc822 = false;
      ae = 0;
      // Split and parse header of entity
      q = core_entity_parser(mpe[i].start, mpe[i].len, &e_h, &e_len);
      if(NULL == q)
      {
         // Invalid entity => Handle whole entity as raw binary octet stream
         PRINT_ERROR("Invalid entity in MIME multipart entity");
         e_h = NULL;
         e_len = mpe[i].len;
         ct.type = ENC_CT_APPLICATION;
         ct.subtype = ENC_CTS_OCTETSTREAM;
         ct.charset = ENC_CS_UNKNOWN;
         ct.flags = 0;
      }
      else
      {
         p = q;
         // Default to "text/plain; charset=US-ASCII" as defined by RFC 2046
         ct.type = ENC_CT_TEXT;
         ct.subtype = ENC_CTS_PLAIN;
         ct.charset = ENC_CS_ASCII;
         ct.flags = 0;
         // Use "message/rfc822" for multipart/digest as defined by RFC 2046
         if(digest)
         {
            ct.type = ENC_CT_MESSAGE;
            ct.subtype = ENC_CTS_RFC822;
            ct.charset = ENC_CS_UNKNOWN;
            ct.flags = 0;
            message_rfc822 = true;
         }
         // Check whether content type header field exist
         if(NULL == e_h->mime_ct)
         {
            if(main_debug)
            {
               if(!digest)
               {
                  printf("%s: %sContent-Type: Text (using default)\n",
                         CFG_NAME, MAIN_ERR_PREFIX);
               }
               else
               {
                  printf("%s: %sContent-Type: Message (using default)\n",
                         CFG_NAME, MAIN_ERR_PREFIX);
               }
            }
         }
         else
         {
            message_rfc822 = false;
            // Yes => Check whether content transfer encoding is supported
            cte = enc_mime_get_cte(e_h->mime_cte);
            if(ENC_CTE_UNKNOWN == cte)
            {
               PRINT_ERROR("Unknown MIME Content-Transfer-Encoding");
               // No => Treat as arbitrary binary data
               cte = ENC_CTE_BIN;
               // RFC 2049 requires that such content must be treated as type
               // "application/octet-stream".
               ct.type = ENC_CT_APPLICATION;
               ct.subtype = ENC_CTS_OCTETSTREAM;
               ct.charset = ENC_CS_UNKNOWN;
            }
            else
            {
               // Yes => Check content type
               enc_mime_get_ct(&ct, e_h->mime_ct, nested_boundary);
               switch(ct.type)
               {
                  case ENC_CT_TEXT:
                  {
                     if(main_debug)
                     {
                        printf("%s: %sContent-Type: Text\n",
                               CFG_NAME, MAIN_ERR_PREFIX);
                     }
                     // Check character set
                     if(ENC_CS_UNKNOWN == ct.charset)
                     {
                        // Content character set not supported
                        // RFC 2049 requires that such content must be treated
                        // as type "application/octet-stream".
                        ct.type = ENC_CT_APPLICATION;
                        ct.subtype = ENC_CTS_OCTETSTREAM;
                        ct.charset = ENC_CS_UNKNOWN;
                     }
                     break;
                  }
                  case ENC_CT_IMAGE:
                  case ENC_CT_AUDIO:
                  case ENC_CT_VIDEO:
                  {
                     if(main_debug)
                     {
                        printf("%s: %sContent-Type: Image, Audio or Video\n",
                               CFG_NAME, MAIN_ERR_PREFIX);
                     }
                     ct.subtype = ENC_CTS_UNKNOWN;
                     break;
                  }
                  case ENC_CT_MULTIPART:
                  {
                     if(main_debug)
                     {
                        printf("%s: %sContent-Type: Multipart (nested)\n",
                               CFG_NAME, MAIN_ERR_PREFIX);
                     }
                     nested_multi = true;
                     if(ENC_CTS_ALTERNATIVE == ct.subtype)
                     {
                        nested_alt = true;
                     }
                     else if(ENC_CTS_DIGEST == ct.subtype)
                     {
                        nested_digest = true;
                     }
                     break;
                  }
                  case ENC_CT_MESSAGE:
                  {
                     if(main_debug)
                     {
                        printf("%s: %sContent-Type: Message\n",
                               CFG_NAME, MAIN_ERR_PREFIX);
                     }
                     if(ENC_CTS_RFC822 == ct.subtype)
                     {
                        message_rfc822 = true;
                     }
                     else
                     {
                        ct.type = ENC_CT_APPLICATION;
                        ct.subtype = ENC_CTS_OCTETSTREAM;
                        ct.charset = ENC_CS_UNKNOWN;
                     }
                     break;
                  }
                  default:
                  {
                     if(main_debug)
                     {
                        printf("%s: %sContent-Type not supported\n",
                               CFG_NAME, MAIN_ERR_PREFIX);
                     }
                     // Handle anything unknown as type
                     // "application/octet-stream"
                     ct.type = ENC_CT_APPLICATION;
                     ct.subtype = ENC_CTS_OCTETSTREAM;
                     ct.charset = ENC_CS_UNKNOWN;
                     break;
                  }
               }
            }
         }
      }
      // Special handling for MIME content type "multipart/alternative"
      if(alt)
      {
         // Check whether more sophisticated alternative is supported
         if(i)
         {
            if(!(ENC_CT_TEXT == ct.type && ENC_CTS_PLAIN == ct.subtype
               && ENC_CTE_UNKNOWN != cte))
            {
               // Ignore this alternative
               core_destroy_entity_header(&e_h);
               continue;
            }
         }
      }
      // Create new content element(s)
      if(nested_multi)
      {
         // Recursively decode nested multipart entities
         cle = decodeElement(p, e_len,
                             nested_boundary, nested_alt, nested_digest, &ae);
      }
      // Create new element for encapsulated header of type message/rfc822
      else if(message_rfc822)
      {
         // Decode encapsulated entity
         nested_boundary[0] = 0;
         mle = decodeElement(p, e_len,
                             nested_boundary, nested_alt, nested_digest, &ae);
         // Create additional list element for header of encapsulated message
         cte = ENC_CTE_8BIT;
         ct.type = ENC_CT_TEXT;
         ct.subtype = ENC_CTS_PLAIN;
         ct.charset = ENC_CS_UTF_8;
         ct.flags = 0;
         q = createMessageHeader(p, len);
         cle = initElement(q, std::strlen(q), cte, &ct, e_h);
         enc_free((void*) q);
         ++ae;
         // Append element for the message header first
         cle->next = mle;
         // Append additional list element for end of encapsulated message
         SC("Control characters for line break at the end must stay in place")
         q = S("End of encapsulated message.\r\n");
         mle = initElement(q, std::strlen(q), cte, &ct, e_h);
         ++ae;
         // Append element for EOM indication at the end
         tmp = cle;
         while(NULL != tmp->next)  { tmp = tmp->next; }
         tmp->next = mle;
      }
      else
      {
         // Create new element
         cle = initElement(p, e_len, cte, &ct, e_h);
         ++ae;
      }
      if(NULL == cle)
      {
         PRINT_ERROR("Decoding MIME multipart message failed");
         break;
      }
      // Special handling for MIME content type "multipart/alternative"
      if(alt)
      {
         // Update current choice
         ble = cle;
         bae = ae;
      }
      else
      {
         // Append new element(s) to linked list
         if(!i)  { res = cle; }  else  { lle->next = cle; }
         // Skip to last element
         lle = cle;
         while(NULL != lle->next)  { lle = lle->next; }
         // Update element count
         *count += ae;
      }
      // End of loop
      core_destroy_entity_header(&e_h);
   }
   if(num)  { enc_free((void*) mpe); }
   // Special handling for MIME content type "multipart/alternative"
   if(alt)
   {
      // Append only the last supported entity to linked list
      res = ble;
      // Update element count
      *count += bae;
   }
   // Debug message when multipart content or encapsulated message ends
   if(main_debug)
   {
      if(std::strlen(boundary))
      {
         printf("%s: %s--- End of MIME multipart entity ---\n",
                CFG_NAME, MAIN_ERR_PREFIX);
      }
      else
      {
         printf("%s: %s^^^ End of MIME encapsulated message ^^^\n",
                CFG_NAME, MAIN_ERR_PREFIX);
      }
   }

   return(res);
}


// =============================================================================
// MIME content element constructor

MIMEContent::MIMEContentListElement*
MIMEContent::initElement(const char*  body, std::size_t  body_len,
                         enc_mime_cte  cte, enc_mime_ct*  ct,
                         struct core_article_header*  hdr)
{
   MIMEContentListElement*  cle = new MIMEContentListElement;
   char*  cp;
   Fl_Text_Buffer  tb;
   bool  headerPresent = false;

   // Content-Transfer-Encoding
   cle->cte = cte;

   // Content-Type
   std::memcpy((void*) &cle->ct, (void*) ct, sizeof(enc_mime_ct));

   // Body
   cp = new char[body_len + (std::size_t) 1];
   std::strncpy(cp, body, body_len);  cp[body_len] = 0;
   cle->content = cp;

   // Entity headers for multipart content
   cle->type = ENC_CD_INLINE;
   cle->filename = NULL;
   cle->header = NULL;
   tb.text("");
   if(NULL != hdr && NULL != hdr->mime_cte)
   {
      tb.append(S("Transfer-Encoding"));
      tb.append(": ");
      tb.append(hdr->mime_cte);
      tb.append("\n");
      headerPresent = true;
   }
   if(NULL != hdr && NULL != hdr->mime_ct)
   {
      tb.append(S("Content-Type"));
      tb.append(": ");
      tb.append(hdr->mime_ct);
      tb.append("\n");
      headerPresent = true;
   }
   if(NULL != hdr && NULL != hdr->mime_cd)
   {
      tb.append(S("Content-Disposition"));
      tb.append(": ");
      tb.append(hdr->mime_cd);
      tb.append("\n");
      headerPresent = true;
      // Store type and filename for entity
      enc_mime_get_cd(hdr->mime_cd, &cle->type, &cle->filename);
   }
   if(headerPresent)
   {
      cle->header = tb.text();
   }

   // Terminate linked list
   cle->next = NULL;

   return(cle);
}


// =============================================================================
// MIME content object constructor

MIMEContent::MIMEContent(struct core_article_header*  h, const char*  p)
{
   char  boundary[ENC_BO_BUFLEN];
   enc_mime_cte  cte = ENC_CTE_BIN;
   enc_mime_ct  ct;
   bool  multi;
   bool  alt;  // Multipart entity contains multiple variants of same content
   bool  digest;  // Default content type for multipart is message/rfc822

   // Init linked list
   partList = NULL;
   partNum = 0;

   // Check for MIME declaration (always assume version 1.0)
   if(NULL == h->mime_v)
   {
      if(NULL != h->mime_cte || NULL != h->mime_ct)
      {
         PRINT_ERROR("Trying to decode MIME article "
                     "with missing MIME-Version header field");
      }
   }
   // Default to "text/plain; charset=US-ASCII" as defined by RFC 2045
   multi = false;
   alt = false;
   digest = false;
   ct.type = ENC_CT_TEXT;
   ct.subtype = ENC_CTS_PLAIN;
   ct.charset = ENC_CS_ASCII;
   ct.flags = 0;
   // Check whether content type header field exist
   if(NULL != h->mime_ct || NULL != h->mime_cte)
   {
      // Yes => Check whether content transfer encoding is supported
      cte = enc_mime_get_cte(h->mime_cte);
      if(ENC_CTE_UNKNOWN == cte)
      {
         // No => Treat as arbitrary binary data
         cte = ENC_CTE_BIN;
         // RFC 2049 requires that such content must be treated as type
         // "application/octet-stream".
         ct.type = ENC_CT_APPLICATION;
         ct.subtype = ENC_CTS_OCTETSTREAM;
         ct.charset = ENC_CS_UNKNOWN;
      }
      else
      {
         // Yes => Check content type
         enc_mime_get_ct(&ct, h->mime_ct, boundary);
         switch(ct.type)
         {
            case ENC_CT_TEXT:
            {
               // Check character set
               if(ENC_CS_UNKNOWN == ct.charset)
               {
                  // Content character set not supported
                  // RFC 2049 requires that such content must be treated as
                  // type "application/octet-stream".
                  ct.type = ENC_CT_APPLICATION;
                  ct.subtype = ENC_CTS_OCTETSTREAM;
                  ct.charset = ENC_CS_UNKNOWN;
               }
               break;
            }
            case ENC_CT_IMAGE:
            case ENC_CT_AUDIO:
            case ENC_CT_VIDEO:
            {
               break;
            }
            case ENC_CT_MULTIPART:
            {
               // The boundary delimiter is already stored in 'boundary'
               // Set flag and split the content later
               multi = true;
               // Set flag if subtype is "alternative"
               if(ENC_CTS_ALTERNATIVE == ct.subtype)  { alt = true; }
               // Set flag if subtype is "digest"
               else if(ENC_CTS_DIGEST == ct.subtype)  { digest = true; }
               break;
            }
            default:
            {
               // Handle anything unknown as type "application/octet-stream"
               ct.type = ENC_CT_APPLICATION;
               ct.subtype = ENC_CTS_OCTETSTREAM;
               ct.charset = ENC_CS_UNKNOWN;
               break;
            }
         }
      }
   }

   // Create linked list of entities
   if(!multi)
   {
      multipart = false;
      partList = initElement(p, std::strlen(p), cte, &ct, h);
      partNum = 1;
   }
   else
   {
      multipart = true;
      // Recursively create elements from multipart entities
      partList = decodeElement(p, std::strlen(p),
                               boundary, alt, digest, &partNum);
   }
}


// =============================================================================
// MIME content object constructor

MIMEContent::~MIMEContent(void)
{
   MIMEContentListElement*  cle = partList;
   MIMEContentListElement*  nle;

   while(NULL != cle)
   {
      nle = cle->next;
      enc_free((void*) cle->filename);
      std::free((void*) cle->header);
      delete[] cle->content;
      delete cle;
      cle = nle;
   }
}


// =============================================================================
// Subscribe tree OK button callback

void  SubscribeWindow::ok_cb_i(void)
{
   Fl_Tree_Item*  i = subscribeTree->first_selected_item();
   int  rv = -1;
   std::size_t  ii;
   char  buf[1024];
   char*  name;

   // Hide root node tree to remove it from pathname
   subscribeTree->showroot(0);

   // Update subscribed groups
   mainWindow->groupStateExport();
   while(NULL != i)
   {
      // Check whether an menu style path was stored in user data
      if('\0' != ((const char*) i->user_data())[0])
      {
         // Yes => Use it
         name = (char*) i->user_data();
         rv = 0;
      }
      else
      {
         // No => Get menu style path from widget
         rv = subscribeTree->item_pathname(buf, 1024, i);
         name = buf;
      }
      if(rv)
      {
         if(-2 == rv)  { PRINT_ERROR("Group name too long and ignored"); }
         else  { PRINT_ERROR("Group not found (bug)"); }
      }
      else
      {
         ii = 0;
         do
         { if('/' == name[ii])  { name[ii] = '.'; } }
         while(name[ii++]);
         // Because the number of selected groups is usually low, we can add
         // them one after the other without relevant performance impact
         rv = core_subscribe_group(name);
         if(rv)
         {
            PRINT_ERROR("Updating list of subscribed groups failed");
            break;
         }
      }
      i = subscribeTree->next_selected_item(i);
      rv = 0;
   }

   // Check result
   if(rv)
   {
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s", S("Error while updating subscribed group database"));
   }
   else  { UI_STATUS(S("Subscribed groups stored.")); }

   // This check should silcene code checking tools
   // ('mainWindow' is always present)
   if(NULL != mainWindow)
   {
      // Import new group list
      mainWindow->groupListImport();
   }

   // Schedule destruction of subscribe window
   Fl::delete_widget(this);
}


// =============================================================================
// Subscribe window constructor

SubscribeWindow::SubscribeWindow(const char*  label, core_groupdesc*  glist,
   core_grouplabel*  labels) :
   UI_WINDOW_CLASS(730, 350, label), grouplist(glist), grouplabels(labels)
{
   Fl_Group*  gp;
   Fl_Box*  bp;
   Fl_Button*  p;
   int  y, w, h, gy, gh, bw;
   int  w1, w2, h1, h2;

   // Configure subscribe window
   copy_label(label);
   callback(cancel_cb, (void*) this);
   set_modal();

   // Add widgets --------------------------------------------------------------
   begin();

   // Create widget group
   subscribeGroup = new Fl_Group(0, 0, 730, 350);
   subscribeGroup->begin();
   {
      // Calculate required widths and heights
      gui_set_default_font();
      fl_measure(S("Cancel"), w1 = 0, h1 = 0);
      fl_measure(S("OK"), w2 = 0, h2 = 0);
      if(w1 > w2)  { w = w1; }  else  { w = w2; }
      if(h1 > h2)  { h = h1; }  else  { h = h2; }
      w += 30;
      h += 10;
      gh = h + 10;
      gy = 350 - gh;
      // Add newsgroup tree
      subscribeTree = new Fl_Tree(0, 0, 730, 350 - gh);
      // Explicitly set foreground color to FLTK default
      // (default is not the same as for the other widgets)
      subscribeTree->item_labelfgcolor(FL_FOREGROUND_COLOR);
      // Special handling for the root item that was already present
      subscribeTree->root()->labelfgcolor(FL_FOREGROUND_COLOR);
      subscribeTree->root_label("Usenet");
      subscribeTree->margintop(5);
      subscribeTree->marginleft(0);
      subscribeTree->openchild_marginbottom(5);
      subscribeTree->selectmode(FL_TREE_SELECT_MULTI);
      subscribeTree->sortorder(FL_TREE_SORT_ASCENDING);
      subscribeTree->callback(tree_cb, (void*) this);
      // Add button bar at bottom
      gp = new Fl_Group(0, gy, 730, gh);
      gp->begin();
      {
         bw = w + 30;
         y = gy + 5;
         new Fl_Box(0, gy, bw, gh);
         p = new Fl_Button(15, y, w, h, S("OK"));
         p->callback(ok_cb, (void*) this);
         new Fl_Box(730 - bw, gy, bw, gh);
         p = new Fl_Button(730 - bw + 15, y, w, h, S("Cancel"));
         p->callback(cancel_cb, (void*) this);
         // Resizable space between buttons
         bp = new Fl_Box(bw, gy, 730 - (2 * bw), gh);
      }
      gp->end();
      gp->resizable(bp);
   }
   subscribeGroup->end();
   subscribeGroup->resizable(subscribeTree);
   resizable(subscribeGroup);

   end();
   // --------------------------------------------------------------------------

   // Set minimum window size
   size_range(2 * bw, 100, 0, 0);
#if !CFG_DB_DISABLE
   Fl::visual(FL_DOUBLE | FL_INDEX);
#endif  // CFG_DB_DISABLE
   show();
}


// =============================================================================
// Subscribe window destructor

SubscribeWindow::~SubscribeWindow(void)
{
   core_free((void*) grouplabels);
   core_free((void*) grouplist);
}


// =============================================================================
// Protocol console update method

void  ProtocolConsole::update(void)
{
   int  rv;
   const char*  logname;
   char  buffer[128];
   unsigned int  n;

   // Update protocol console
   if(!nolog)
   {
      if(!logfp)
      {
         logname = log_get_logpathname();
         if(logname)
         {
            logfp = fopen(logname, "r");
            log_free((void*) logname);  logname = NULL;
            if(!logfp)
            {
               nolog = 1;
               protocolConsole->consoleDisplay->insert(
                  S("Logfile is only available in debug mode.") );
               protocolConsole->consoleDisplay->insert("\n");
               protocolConsole->consoleDisplay->insert(
                  S("Use command line option -debug for debug mode.") );
            }
         }
      }
      else
      {
         while(!feof(logfp))
         {
            for(n = 0; n < sizeof(buffer) - 1; ++n)
            {
               rv = fgetc(logfp);
               if(EOF == rv)  { break; }
               else
               {
                  // Replace CR characters with spaces
                  if(0x0D == rv)  { buffer[n] = 0x20; }
                  else  { buffer[n] = (char) (unsigned char) rv; }
               }
            }
            buffer[n] = 0;
            protocolConsole->consoleDisplay->insert(buffer);
            // Drive GUI
            Fl::check();
            // The protocol console was possibly destroyed by a callback
            // Verify that it's still present
            if(NULL == protocolConsole)  { break; }
         }
         if(protocolConsole)  { clearerr(logfp); }
      }
   }
}


// =============================================================================
// Protocol console constructor

ProtocolConsole::ProtocolConsole(const char*  label) :
   UI_WINDOW_CLASS(730, 395, label), logfp(NULL), nolog(0)
{
   copy_label(label);
   consoleText = new Fl_Text_Buffer();
   consoleDisplay = new Fl_Text_Display(0, 0, 730, 395);
   consoleDisplay->buffer(consoleText);
   resizable(consoleDisplay);
   callback(exit_cb, (void*) this);
#if !CFG_DB_DISABLE
   Fl::visual(FL_DOUBLE | FL_INDEX);
#endif  // CFG_DB_DISABLE
   show();
}


// =============================================================================
// Protocol console destructor

ProtocolConsole::~ProtocolConsole(void)
{
   if(logfp)  { fclose(logfp); }

   // Release memory
   delete consoleDisplay;
   delete consoleText;

   protocolConsole = NULL;
}


// =============================================================================
// Message-ID search window callback

void  MIDSearchWindow::ok_cb_i(void)
{
   std::size_t  limit = 248;  // Octets (without angle brackets)
   const char*  message_id = mid->value();
   char*  buf = NULL;
   char*  p;
   std::size_t  len;

   // Copy Message-ID
   len = std::strlen(message_id);
   buf = new char[len + (std::size_t) 1];
   std::strcpy(buf, message_id);

   // Remove potential surrounding whitespace and angle brackets
   p = buf;
   while(0x09 == (int) p[0] || ' ' == p[0] || '<' == p[0])
   {
      p = &p[1];
      --len;
   }
   while(0x09 == (int) p[len - (std::size_t) 1]
         || ' ' == p[len - (std::size_t) 1] || '>' == p[len - (std::size_t) 1])
   {
      p[len - (std::size_t) 1] = 0;
      --len;
   }

   // Check length limit
   if(limit < len)
   {
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s",
   S("Message-ID too long\nLimit is 250 characters, including angle brackets"));
   }

   // Destroy configuration window
   Fl::delete_widget(this);

   // Search article (and display it in separate window if found)
   mainWindow->viewArticle(UI_CB_START, (const char*) p);
   delete[]  buf;
}


// =============================================================================
// Message-ID search window constructor

MIDSearchWindow::MIDSearchWindow(const char*  label) :
   UI_WINDOW_CLASS(700, 150, label)
{
   Fl_Group*  gp1;
   Fl_Group*  gp2;
   Fl_Box*  bp;
   Fl_Button*  p;
   int  y, w, h, gy, gh, bw;
   int  w1, w2, h1, h2;

   // Configure Message-ID search window
   copy_label(label);
   set_modal();
   callback(cancel_cb, (void*) this);

   // Create widget group
   cfgGroup = new Fl_Group(0, 0, 700, 150);
   cfgGroup->begin();
   {
      // Calculate required widths and heights
      gui_set_default_font();
      fl_measure(S("Cancel"), w1 = 0, h1 = 0);
      fl_measure(S("OK"), w2 = 0, h2 = 0);
      if(w1 > w2)  { w = w1; }  else  { w = w2; }
      if(h1 > h2)  { h = h1; }  else  { h = h2; }
      w += 30;
      h += 10;
      gh = h + 10;
      gy = 150 - gh;

      gp1 = new Fl_Group(0, 0, 700, gy);
      gp1->begin();
      {
         // Add "From" Name field
         mid = new Fl_Input(15, 35, 670, 30, "Message-ID:");
         mid->align(FL_ALIGN_TOP_LEFT);
         // Resizable space below input fields
         bp = new Fl_Box(0, 100, 700, gy - 100);
         //bp->box(FL_DOWN_BOX);
      }
      gp1->end();
      gp1->resizable(bp);
      // Add button bar at bottom
      gp2 = new Fl_Group(0, gy, 700, gh);
      gp2->begin();
      {
         bw = w + 30;
         y = gy + 5;
         new Fl_Box(0, gy, bw, gh);
         p = new Fl_Return_Button(15, y, w, h, S("OK"));
         p->callback(ok_cb, (void*) this);
         new Fl_Box(500 - bw, gy, bw, gh);
         p = new Fl_Button(700 - bw + 15, y, w, h, S("Cancel"));
         p->callback(cancel_cb, (void*) this);
         p->shortcut(FL_Escape);

         // Resizable space between buttons
         bp = new Fl_Box(bw, gy, 700 - (2 * bw), gh);
      }
      gp2->end();
      gp2->resizable(bp);
   }
   cfgGroup->end();
   cfgGroup->resizable(gp1);
   resizable(cfgGroup);
   // Set minimum window size
   size_range(700, 100 + gh, 0, 0);
#if !CFG_DB_DISABLE
   Fl::visual(FL_DOUBLE | FL_INDEX);
#endif  // CFG_DB_DISABLE
   show();
}


// =============================================================================
// Message-ID search window destructor

MIDSearchWindow::~MIDSearchWindow(void)
{
}


// =============================================================================
// Bug report window constructor

BugreportWindow::BugreportWindow(const char*  label, const char*  content) :
   UI_WINDOW_CLASS(795, 395, label)
{
   std::size_t  len;

   copy_label(label);
   len = std::strlen(content);
   if(INT_MAX < len)  { len = 0; }
   bugreportText = new Fl_Text_Buffer((int) ++len, 0);
   bugreportText->insert(0, content);
   bugreportDisplay = new Fl_Text_Display(0, 0, 795, 395);
   bugreportDisplay->textfont(FL_COURIER);
   bugreportDisplay->buffer(bugreportText);
   resizable(bugreportDisplay);
   callback(exit_cb, (void*) this);
#if !CFG_DB_DISABLE
   Fl::visual(FL_DOUBLE | FL_INDEX);
#endif  // CFG_DB_DISABLE
   show();
}


// =============================================================================
// Bug report window destructor

BugreportWindow::~BugreportWindow(void)
{
   // Release memory
   delete bugreportDisplay;
   delete bugreportText;
}


// =============================================================================
// Message of the day window constructor

MotdWindow::MotdWindow(const char*  motd, const char*  label) :
   UI_WINDOW_CLASS(795, 395, label)
{
   copy_label(label);
   motdText = new Fl_Text_Buffer();
   motdText->text(motd);
   motdDisplay = new Fl_Text_Display(0, 0, 795, 395);
   motdDisplay->textfont(FL_COURIER);
   motdDisplay->buffer(motdText);
   resizable(motdDisplay);
   callback(exit_cb, (void*) this);
#if !CFG_DB_DISABLE
   Fl::visual(FL_DOUBLE | FL_INDEX);
#endif  // CFG_DB_DISABLE
   show();
}


// =============================================================================
// Message of the day  window destructor

MotdWindow::~MotdWindow(void)
{
   // Release memory
   delete motdDisplay;
   delete motdText;
}


// =============================================================================
// License window constructor

LicenseWindow::LicenseWindow(const char*  label) :
   UI_WINDOW_CLASS(795, 395, label)
{
   int  rv;

   copy_label(label);
   licenseText = new Fl_Text_Buffer();
   rv = licenseText->loadfile(CFG_LICENSE_PATH "/license.txt");
   if(rv)  { licenseText->insert(0, S("Error: License file not found")); }
   licenseDisplay = new Fl_Text_Display(0, 0, 795, 395);
   licenseDisplay->textfont(FL_COURIER);
   licenseDisplay->buffer(licenseText);
   resizable(licenseDisplay);
   callback(exit_cb, (void*) this);
#if !CFG_DB_DISABLE
   Fl::visual(FL_DOUBLE | FL_INDEX);
#endif  // CFG_DB_DISABLE
   show();
}


// =============================================================================
// License window destructor

LicenseWindow::~LicenseWindow(void)
{
   // Release memory
   delete licenseDisplay;
   delete licenseText;
}


// =============================================================================
// Article window format and print some header fields of article

const char*  ArticleWindow::printHeaderFields(struct core_article_header*  h)
{
   return(gui_print_header_fields(h));
}


// =============================================================================
// Article window update

void  ArticleWindow::articleUpdate(Fl_Text_Buffer*  article)
{
   // See RFC 3986 for URI format
   const char*  url[] = { "http://", "https://", "ftp://", "nntp://",
                          "news:", "mailto:", NULL };
   const std::size_t  url_len[] = { 7, 8, 6, 7, 5, 7 };
   const char  bold = 'A';   // Bold text for header field names
   const char  sig = 'B';    // Signature starting with "-- " separator
   const char  cit = 'C';    // External citation ("|" at start of line)
   const char  link = 'D';   // Hyperlink
   const char  plain = 'E';  // Normal article text
   const char  l1 = 'F';     // 1st citation level
   const char  l2 = 'G';     // 2nd citation level
   const char  l3 = 'H';     // 3rd citation level
   const char  l4 = 'I';     // 4th citation level
   char*  style;
   std::size_t  len;
   std::size_t  i;
   std::size_t  ii = 0;  // Index in current line
   std::size_t  iii = 0;
   std::size_t  iiii;
   std::size_t  url_i;
   bool  sol = true;  // Start Of Line flag
   char  hs = plain;
   bool  ready = false;  // Flag indicating positions beyond header separator
   int  ss = 0;
   bool  delim = false;  // Hyperlink delimiter
   bool  signature = false;  // Flag indicating signature
   bool  citation = false;  // Flag indicating external citation
   bool  hyperlink = false;  // Flag indicating hyperlink
   std::size_t  cl = 0;
   bool  cl_lock = false;
   char  c;
   int  pos;

   // Replace current article
   if(NULL == article)
   {
      PRINT_ERROR("Article display request without content ignored (bug)");
      return;
   }
   gui_check_article(article);
   articleText = article;
   articleDisplay->buffer(articleText);

#if !UI_AW_REFERENCES
   // Remove references line for ArticleWindow (not useful without hyperlinks)
   if (1 == articleText->findchar_forward(0, 0x1DU, &pos))
   {
      articleText->remove(articleText->line_start(pos),
                          articleText->line_end(pos) + 1);
   }
#endif  // ! UI_AW_REFERENCES

   // Soft Hyphen (SHY) handling
   gui_process_shy(articleText);

   // Create article content style
   style = articleText->text();
   if(NULL == style)  { len = 0; }
   else  { len = std::strlen(style); }
   if(INT_MAX < len)  { len = INT_MAX; }
   for(i = 0; i < len; ++i)
   {
      if('\n' == style[i])
      {
         sol = true;
         hyperlink = false;
         citation = false;
         continue;
      }
      if(hyperlink)
      {
         // Check for end of hyperlink
         // According to RFC 3986 whitespace, double quotes and angle brackets
         // are accepted as delimiters.
         // Whitespace is interpreted as SP or HT, Unicode whitespace is not
         // accepted as delimiter.
         c = style[i];
         if(' ' == c || 0x09 == (int) c || '>' == c || '"' == c)
         {
            hyperlink = false;
         }
      }
      // Highlight external citations (via '|' or '!')
      if(sol && ('|' == style[i] || '!' == style[i]))
      {
         if(!hyperlink)  { citation = true; }
      }
      // Check for start of line
      if(sol)  { sol = false;  delim = true;  ss = 0;  cl = 0;  ii = 0; }
      else  { ++ii; }
      if(signature)
      {
         // Check for end of signature in potential multipart message
         if('|' == style[i])
         {
            if(79U == ii)
            {
               for(iiii = i - ii; iiii < i; ++iiii)
               {
                  if('_' != articleText->byte_at((int) iiii))  { break; }
               }
               if(iiii == i)
               {
                  for(iiii = 0; iiii <= ii; ++iiii)
                  {
                     style[i - iiii] = plain;
                  }
                  signature = false;
               }
            }
         }
      }
      if(!ready)
      {
         // Check for header separator <SOL>"____...____|"
         if(!ii)
         {
            if('_' != style[i])  { hs = bold; }  else  { hs = plain; }
            iii = 0;
         }
         if('|' == style[i] && 79U <= iii)  { ready = true; }
         else if('_' == style[i])  { ++iii; }
         if(':' == style[i])  { hs = plain; }
         style[i] = hs;
      }
      else
      {
         // Check for signature separator <SOL>"-- "
         if('-' == style[i] && !ii)  { ss = 1; }
         if('-' == style[i] && 1U == ii && 1 == ss)  { ss = 2; }
         if(' ' == style[i] && 2U == ii && 2 == ss)
         {
            // Attention: This EOL check requires POSIX line format!
            if((char) 0x0A == style[i + 1U])
            {
               if(gui_last_sig_separator(&style[i + 1U]))
               {
                  style[i] = sig;  style[i - 1U] = sig;  style[i - 2U] = sig;
                  signature = true;
               }
            }
         }
         // Check for hyperlink
         url_i = 0;
         while(NULL != url[url_i])
         {
            if(!std::strncmp(&style[i], url[url_i], url_len[url_i]))
            {
               if(delim)
               {
                  style[i] = link;
                  // ArticleWindow does not support hyperlinks
                  //hyperlink = true;
               }
            }
            ++url_i;
         }
         c = style[i];
         if(' ' == c || 0x09 == (int) c || '<' == c || '"' == c)
         {
            delim = true;
         }
         else  { delim = false; }
         if(1)
         {
            // Highlight citation levels of regular content
            if(!ii)
            {
               if('>' == style[i])  { cl_lock = false; }
               else  { cl_lock = true; }
            }
            if('>' == style[i] && !cl_lock)  { ++cl; }
            if('>' != style[i] && ' ' != style[i] && (char) 9 != style[i])
            {
               cl_lock = true;
            }
            if(4U < cl)  { cl = 1; }  // Rotate colors if too many levels
            switch(cl)
            {
               case 1:  { style[i] = l1;  break; }
               case 2:  { style[i] = l2;  break; }
               case 3:  { style[i] = l3;  break; }
               case 4:  { style[i] = l4;  break; }
               default:  { style[i] = plain;  break; }
            }
         }
         // Override current style for signature, citations and hyperlinks
         // (hyperlinks have highest precedence)
         if(signature)  { style[i] = sig; }
         if(citation && !signature)  { style[i] = cit; }
         if(hyperlink)  { style[i] = link; }
      }
   }
   articleStyle = new Fl_Text_Buffer((int) len, 0);
   if(NULL != style)  { articleStyle->text(style); }
   std::free((void*) style);

   // Activate content style
   articleDisplay->highlight_data(articleStyle, styles, styles_len, 'A', NULL,
                                  NULL);

   // Replace potential GS marker with SP
   if (1 == articleText->findchar_forward(0, 0x1DU, &pos))
   {
      articleText->replace(pos, pos + 1, " ");
   }
}


// =============================================================================
// Article window constructor

ArticleWindow::ArticleWindow(const char*  article, const char*  label) :
   UI_WINDOW_CLASS(795, 395, label)
{
   Fl_Color  signature = conf_integer_check(CONF_COLOR_SIGNATURE, 0x00, 0xFF);
   Fl_Color  external = conf_integer_check(CONF_COLOR_EXTERNAL, 0x00, 0xFF);
   Fl_Color  hyperlink = conf_integer_check(CONF_COLOR_HYPERLINK, 0x00, 0xFF);
   Fl_Color  level1 = conf_integer_check(CONF_COLOR_LEVEL1, 0x00, 0xFF);
   Fl_Color  level2 = conf_integer_check(CONF_COLOR_LEVEL2, 0x00, 0xFF);
   Fl_Color  level3 = conf_integer_check(CONF_COLOR_LEVEL3, 0x00, 0xFF);
   Fl_Color  level4 = conf_integer_check(CONF_COLOR_LEVEL4, 0x00, 0xFF);
   const Fl_Color  styles_colors[UI_STYLES_LEN] =
   {
      FL_FOREGROUND_COLOR,             // Plain bold
      signature,                       // Signature
      external,                        // External citation
      hyperlink,                       // Hyperlink
      // -----------------------------------------------------------------------
      // Keep the following group continuous
      FL_FOREGROUND_COLOR,             // Content
      level1,                          // Thread citation level 1 (">  ")
      level2,                          // Thread citation level 2 ("> >  ")
      level3,                          // Thread citation level 3 ("> > >  ")
      level4                           // Thread citation level 4 ("> > > >  ")
   };
   Fl_Group*  gp;
   Fl_Box*  bp;
   Fl_Button*  buttonp;
   int  y, w, h, gy, gh, bw;
   int  rv = -1;
   char*  raw;
   char*  eoh;
   const char*  q = NULL;
   Fl_Text_Buffer*  tb;
   const char*  hdr = NULL;
   int  ii;

   copy_label(label);

   // Init alternate article hierarchy pointer
   alt_hier = NULL;

   // Init text buffer pointers
   articleText = NULL;
   articleStyle = NULL;
   styles = NULL;

   // Init MIME object pointer
   mimeData = NULL;

   // Create new article hierarchy
   rv = core_hierarchy_manager(&alt_hier, CORE_HIERARCHY_INIT, 0);
   if(!rv)
   {
      // Add article to alternative hierarchy
      rv = core_hierarchy_manager(&alt_hier, CORE_HIERARCHY_ADD, 1, article);
   }
   if(!rv)
   {
      raw = new char[std::strlen(article) + (std::size_t) 1];
      std::strcpy(raw, article);
      // Search for end of header
      // Attention: Overloaded and both prototypes different than in C!
      eoh = std::strstr(raw, "\r\n\r\n");
      if(NULL == eoh)
      {
         // Body not found
         PRINT_ERROR("Processing of article failed");
         SC("Do not use non-ASCII for the translation of this item")
         fl_message_title(S("Error"));
         fl_alert("%s", S("Processing of article failed"));
         delete[] raw;
      }
      else
      {
         // Set pointer to body data
         q = &eoh[4];
         // Create text buffer
         tb = new Fl_Text_Buffer(0, 0);
         // Print formatted header data to text buffer
         hdr = printHeaderFields(alt_hier->child[0]->header);
         if(NULL != hdr)
         {
            tb->text(hdr);
            delete[] hdr;
         }
         // Print header/body delimiter
         tb->append(ENC_DELIMITER);

         // Create MIME object from content
         mimeData = new MIMEContent(alt_hier->child[0]->header, q);
         delete[] raw;
         gui_decode_mime_entities(tb, mimeData,
                                  alt_hier->child[0]->header->msgid);

         // Configure article window
         callback(cancel_cb, (void*) this);
         set_modal();

         // Add widgets --------------------------------------------------------
         begin();

         // Create widget group
         articleGroup = new Fl_Group(0, 0, 795, 395);
         articleGroup->begin();
         {
            // Calculate required widths and heights
            gui_set_default_font();
            fl_measure(S("Cancel"), w = 0, h = 0);
            w += 30;
            h += 10;
            gh = h + 10;
            gy = 395 - gh;
            // Add text field
            articleDisplay = new My_Text_Display(0, 0, 795, 395 - gh);
            articleDisplay->textfont(FL_COURIER);
            // Allocate and init styles
            styles_len = UI_STYLES_LEN;
            styles = new Fl_Text_Display::Style_Table_Entry[styles_len];
            styles[0].color = FL_FOREGROUND_COLOR;
            styles[0].font = FL_COURIER_BOLD;
            styles[0].size = articleDisplay->textsize();
            styles[0].attr = 0;
            for(ii = 1; ii < styles_len; ++ii)
            {
               styles[ii].color = styles_colors[ii];
               styles[ii].font = articleDisplay->textfont();
               styles[ii].size = articleDisplay->textsize();
               styles[ii].attr = 0;
            }
            // Set text buffer
            articleUpdate(tb);
            resizable(articleDisplay);
            // Add button bar at bottom
            gp = new Fl_Group(0, gy, 795, gh);
            gp->begin();
            {
               bw = w + 30;
               y = gy + 5;
               new Fl_Box(0, gy, bw, gh);
               new Fl_Box(795 - bw, gy, bw, gh);
               buttonp = new Fl_Button(795 - bw + 15, y, w, h, S("Cancel"));
               buttonp->callback(cancel_cb, (void*) this);
               // Resizable space between buttons
               bp = new Fl_Box(bw, gy, 795 - (2 * bw), gh);
            }
            gp->end();
            gp->resizable(bp);
         }
         articleGroup->end();
         articleGroup->resizable(articleDisplay);
         resizable(articleGroup);

         end();
         // --------------------------------------------------------------------

         // Set minimum window size
         size_range(2 * bw, 100, 0, 0);
#if !CFG_DB_DISABLE
         Fl::visual(FL_DOUBLE | FL_INDEX);
#endif  // CFG_DB_DISABLE
         show();
      }
   }
}


// =============================================================================
// Article source window destructor

ArticleWindow::~ArticleWindow(void)
{
   if(NULL != mimeData)  { delete mimeData; }
   // Detach text buffers and destroy them
   articleDisplay->highlight_data(dummyTb, NULL, 0, 'A', NULL, NULL);
   if(articleStyle)  delete articleStyle;
   if(styles)  { delete[] styles; }
   articleDisplay->buffer(dummyTb);
   if(articleText)  delete articleText;
   // Destroy alternative article hierarchy
   core_hierarchy_manager(&alt_hier, CORE_HIERARCHY_INIT, 0);
}


// =============================================================================
// Article source window save file

void  ArticleSrcWindow::save_cb_i(void)
{
   int  rv;

   SC("Do not use characters for the translation that cannot be")
   SC("converted to the ISO 8859-1 character set for this item.")
   SC("Leave the original string in place if in doubt.")
   const char*  title = S("Save article source code");
   const char*  suggested_pathname = core_suggest_pathname();

   fl_file_chooser_ok_label(S("Save"));
   if(NULL != suggested_pathname)
   {
      pathname = fl_file_chooser(title, "*", suggested_pathname, 0);
      if(NULL != pathname)
      {
         // Ask user for overwrite permission if file exists
         rv = gui_check_pathname(pathname);
         if (0 == rv)
         {
            // Save to file
            rv = core_save_to_file(pathname, srcArticle);
            if(rv)
            {
               SC("Do not use non-ASCII for the translation of this item")
               fl_message_title(S("Error"));
               fl_alert("%s", S("Operation failed"));
            }
         }
      }
      core_free((void*) suggested_pathname);
   }
}


// =============================================================================
// Article source window constructor

ArticleSrcWindow::ArticleSrcWindow(const char*  article, const char*  label) :
   UI_WINDOW_CLASS(795, 395, label)
{
   Fl_Group*  gp;
   Fl_Box*  bp;
   Fl_Button*  p;
   int  y, w, h, gy, gh, bw;
   int  w1, w2, h1, h2;
   std::size_t  len;
   const char*  ap = NULL;
   int  i = 0;
   int  buflen;
   unsigned int  octet;
   char  num[3];
   const char*  style;
   int  toggle = 0;

   copy_label(label);

   // Copy raw article content to local memory for "Save to file" operation
   len = std::strlen(article);
   srcArticle = new char[++len];
   std::strcpy(srcArticle, article);

   // Convert article content line breaks from canonical (RFC 822) form
   // to POSIX form
   srcText = new Fl_Text_Buffer();
   srcStyle = new Fl_Text_Buffer();
   ap = core_convert_canonical_to_posix(article, 0, 0);
   if(NULL == ap)  { srcText->insert(0, "Conversion to POSIX form failed"); }
   else
   {
      srcText->insert(0, ap);
      // Replace control/non-ASCII octets with colored hexadecimal numbers
      buflen = srcText->length();
      while(buflen > i)
      {
         srcStyle->insert(i, "A");
         octet = (unsigned int) (unsigned char) srcText->byte_at(i);
         num[0] = (char) (unsigned char) octet;
         num[1] = 0;
         if( (0x0AU != octet && enc_ascii_check_printable(num))
             || 0x09U == octet )
         {
            enc_convert_octet_to_hex(num, octet);
            srcText->replace(i, i + 1, num);
            if(!toggle)  { style = "BB"; }  else  { style = "CC"; }
            srcStyle->replace(i, i + 1, style);
            buflen = srcText->length();
            ++i;
            toggle = !toggle;
         }
         else  { toggle = 0; }
         ++i;
      }
      core_free((void*) ap);
      gui_check_article(srcText);
   }

   // Configure article source code window
   callback(cancel_cb, (void*) this);
   set_modal();

   // Add widgets --------------------------------------------------------------
   begin();

   // Create widget group
   srcGroup = new Fl_Group(0, 0, 795, 395);
   srcGroup->begin();
   {
      // Calculate required widths and heights
      gui_set_default_font();
      fl_measure(S("Cancel"), w1 = 0, h1 = 0);
      fl_measure(S("Save"), w2 = 0, h2 = 0);
      if(w1 > w2)  { w = w1; }  else  { w = w2; }
      if(h1 > h2)  { h = h1; }  else  { h = h2; }
      w += 30;
      h += 10;
      gh = h + 10;
      gy = 395 - gh;
      // Add text field
      srcDisplay = new Fl_Text_Display(0, 0, 795, 395 - gh);
      srcDisplay->textfont(FL_COURIER);
      srcDisplay->buffer(srcText);
      resizable(srcDisplay);
      // Create content and style buffers
      styles = new Fl_Text_Display::Style_Table_Entry[3];
      styles[0].color = FL_FOREGROUND_COLOR;
      styles[0].font = FL_COURIER;
      styles[0].size = srcDisplay->textsize();
      styles[0].attr = 0;
      styles[1].color = FL_COLOR_CUBE + (Fl_Color) 35;
      styles[1].font = FL_COURIER;
      styles[1].size = srcDisplay->textsize();
      styles[1].attr = 0;
      styles[2].color = FL_RED;
      styles[2].font = FL_COURIER;
      styles[2].size = srcDisplay->textsize();
      styles[2].attr = 0;
      srcDisplay->highlight_data(srcStyle, styles, 3, 'A', NULL, NULL);
      // Add button bar at bottom
      gp = new Fl_Group(0, gy, 795, gh);
      gp->begin();
      {
         bw = w + 30;
         y = gy + 5;
         new Fl_Box(0, gy, bw, gh);
         p = new Fl_Button(15, y, w, h, S("Save"));
         p->callback(save_cb, (void*) this);
         new Fl_Box(795 - bw, gy, bw, gh);
         p = new Fl_Button(795 - bw + 15, y, w, h, S("Cancel"));
         p->callback(cancel_cb, (void*) this);
         // Resizable space between buttons
         bp = new Fl_Box(bw, gy, 795 - (2 * bw), gh);
      }
      gp->end();
      gp->resizable(bp);
   }
   srcGroup->end();
   srcGroup->resizable(srcDisplay);
   resizable(srcGroup);

   end();
   // --------------------------------------------------------------------------

   // Set minimum window size
   size_range(2 * bw, 100, 0, 0);
#if !CFG_DB_DISABLE
   Fl::visual(FL_DOUBLE | FL_INDEX);
#endif  // CFG_DB_DISABLE
   show();
}


// =============================================================================
// Article source window destructor

ArticleSrcWindow::~ArticleSrcWindow(void)
{
   // Detach text buffers and destroy them
   srcDisplay->highlight_data(dummyTb, NULL, 0, 'A', NULL, NULL);
   delete srcStyle;
   delete[] styles;
   srcDisplay->buffer(dummyTb);
   delete srcText;
   delete[] srcArticle;
}


// =============================================================================
// Compose window header parser
//
// \param[in]  name   Header field name
// \param[out] start  Start of header field (BOL)
// \param[out] end    End of header field (LF character at EOL)
//
// This method searches the header field \e name in the article header.

int  ComposeWindow::searchHeaderField(const char*  name, int*  start, int*  end)
{
   int  res = -1;
   int  rv;
   unsigned int  c = (unsigned int) '\n';
   int  sp = 0;

   while(1)
   {
      rv = compHeader->search_forward(sp, name, start, 1);
      if(1 == rv)
      {
         // Name match found => Verify that it starts at BOL
         if(0 < *start)  { c = compHeader->char_at(*start - 1); }
         if((unsigned int) '\n' != c)
         {
            // No => Continue searching
            sp = *start + 1;
            continue;
         }
         else
         {
            // Yes => Search for EOL
            rv = compHeader->findchar_forward(*start, (unsigned int) '\n', end);
            if(1 == rv)  { res = 0; }
            // Check for folding and include potential folded lines
            while(!res)
            {
               if((unsigned int) ' ' == compHeader->char_at(*end + 1)
                  && (unsigned int) '\n' != compHeader->char_at(*end + 2))
               {
                  rv = compHeader->findchar_forward(*end + 1,
                                                    (unsigned int) '\n', end);
                  if(1 != rv)  { res = -1; }
               }
               else  { break; }
            }
         }
      }
      break;
   }

   return(res);
}

// =============================================================================
// Compose window header parser
//
// \param[in] name  Header field name
//
// This method extracts the header field "name" from the article header.
//
// On success the caller is responsible for releasing the memory allocated for
// the result using \c std::free() .
//
// \returns
// - Pointer to result buffer
// - NULL on error

const char*  ComposeWindow::extractHeaderField(const char*  name)
{
   char*  res = NULL;
   int  rv;
   int  start;
   int  end;
   int  pos;

   rv = searchHeaderField(name, &start, &end);
   if(!rv)
   {
      // Search for start of header field body
      rv = compHeader->search_forward(start, ": ", &pos, 1);
      if(1 == rv)
      {
         pos += 2;
         res = compHeader->text_range(pos, end);
      }
   }

   return(res);
}


// =============================================================================
// Compose window header parser
//
// \param[in]  name      Header field name
// \param[in]  new_body  New body for header field \e name
//
// This method replaces the body of the header field \e name in the article
// header.

int  ComposeWindow::replaceHeaderField(const char*  name, const char*  new_body)
{
   int  res = -1;
   int  rv;
   int  start;
   int  end;
   int  pos;

   rv = searchHeaderField(name, &start, &end);
   if(!rv)
   {
      // Search for start of header field body
      rv = compHeader->search_forward(start, ": ", &pos, 1);
      if(1 == rv)
      {
         pos += 2;
         if(end >= pos)
         {
            // Replace header field body
            compHeader->replace(pos, end, new_body);
            res = 0;
         }
      }
   }

   return(res);
}


// =============================================================================
// Compose window header parser
//
// \param[in]  name      Header field name
//
// This method deletes the header field \e name in the article header.

void  ComposeWindow::deleteHeaderField(const char*  name)
{
   int  rv;
   int  start;
   int  end;

   rv = searchHeaderField(name, &start, &end);
   if(!rv)  { compHeader->remove(start, end + 1); }

   return;
}


// =============================================================================
// Compose window body checker
//
// \param[in]  body  Article body to check
//
// This method verify that \e body contains own content.

int  ComposeWindow::checkArticleBody(const char*  body)
{
   int  res = -1;
   const char*  p;
   const char*  sig_delim;
   const char*  sig = NULL;
   std::size_t  len;
   std::size_t  i = 0;
   bool  sol = false;  // Start Of Line flag (ignore first line)
   bool  check = false;
   std::size_t  lines = 0;

   if(NULL != body)
   {
      // Calculate length without signature
      len = std::strlen(body);
      p = body;
      while(1)
      {
         // Attention: Overloaded and both prototypes different than in C!
         sig_delim = std::strstr(p, "\n-- \n");
         if(NULL == sig_delim)  { break; }
         else { sig = p = &sig_delim[1]; }
      }
      if(NULL != sig)  { len = (std::size_t) (sig - body); }
      // Search for own (not cited, non-whitespace) content
      for(i = 0; i < len; ++i)
      {
         if(0x0A == (int) body[i])  { sol = true;  check = false;  continue; }
         if(sol)  { ++lines; }
         if(sol && '>' != body[i])  { check = true; }
         sol = false;
         if(check)
         {
            // Check content of non-citation line
            // SP and HT is not treated as relevant content
            if(' ' != body[i] && 0x09 != (int) body[i])  { res = 0;  break; }
         }
      }
      // Special check for messages that contain only one line
      if(res && (std::size_t) 1 >= lines && '>' != body[0])  { res = 0; }
   }

   return(res);
}


// =============================================================================
// Compose window Change subject button callback

void  ComposeWindow::change_cb_i(void)
{
   const char*  subject;
   const char*  subject_new;
   char*  p;
   const char*  q;
   std::size_t  len = 8;  // Length of " (was: )"

   // Get old subject
   subject = subjectField->value();
   // Get new subject
   subject_new = fl_input("%s", "", S("New subject:"));
   if(NULL != subject_new)
   {
      if(std::strlen(subject_new))
      {
         // Check whether subject is changed again
         // Attention: Overloaded and both prototypes different than in C!
         q = std::strstr(subject, " (was: ");
         if(NULL != q)
         {
            // Yes => Preserve " (was: ..." part in parenthesis
            len += std::strlen(subject_new) + std::strlen(q);
            p = new char[++len];
            std::strcpy(p, subject_new);
            std::strcat(p, q);
            subjectField->value(p);
            delete[] p;
         }
         else
         {
            // No  => Strip potential "Re: " prefix from old subject
            // Attention: Overloaded and both prototypes different than in C!
            q = std::strstr(subject, "Re: ");
            if(NULL != q)  { subject = &subject[4]; }
            len += std::strlen(subject_new) + std::strlen(subject);
            p = new char[++len];
            std::strcpy(p, subject_new);
            std::strcat(p, " (was: ");
            std::strcat(p, subject);
            std::strcat(p, ")");
            subjectField->value(p);
            delete[] p;
         }
      }
   }
}


// =============================================================================
// Compose window style update callback

void  ComposeWindow::style_update_cb_i(int  pos, int  nInserted, int  nDeleted,
                                       Fl_Text_Buffer*  style,
                                       Fl_Text_Editor*  editor)
{
   char  sbuf[UI_STATIC_STYLE_BUFSIZE + (std::size_t) 1];
   char*  buf;
   int  start, end;
   int  lines;
   int  i;
   std::size_t  len;
   int  pil;
   int  next;
   std::size_t  p72, p78;

   // Check for parameter integrity
   if(0 > nInserted || 0 > nDeleted)
   {
      PRINT_ERROR("Invalid parameter in compose window style update CB");
      return;
   }

   // If this is just a selection change => Ignore
   if(!nInserted && !nDeleted)  { return; }

   // Check whether text was inserted or deleted
   if(nInserted)
   {
      // Use buffer on stack if new data is small enough
      if(UI_STATIC_STYLE_BUFSIZE < (std::size_t) nInserted)
      {
         buf = new char[(std::size_t) (nInserted + 1)];
      }
      else  { buf = sbuf; }
      std::memset((void*) buf, 'A', (std::size_t) nInserted);
      buf[nInserted] = 0;
      style->replace(pos, pos + nDeleted, buf);
      if(UI_STATIC_STYLE_BUFSIZE < (std::size_t) nInserted)  { delete[] buf; }
   }
   else if(nDeleted)
   {
      style->remove(pos, pos + nDeleted);
   }

   // Restyle new data and up to next linefeed
   start = editor->buffer()->line_start(pos);
   end = editor->buffer()->line_end(pos + nInserted);
   lines = 1 + editor->buffer()->count_lines(pos, end);
   for(i = 0; i < lines; ++i)
   {
      buf = editor->buffer()->line_text(start);
      if(NULL == buf) { continue; }
      len = std::strlen(buf);
      if(len)
      {
         std::memset((void*) buf, 'A', len);

         next = start;
         pil = 0;
         p72 = 0;
         p78 = len;
         while((std::size_t) next - (std::size_t) start < len)
         {
            if(72 == pil)  { p72 = (std::size_t) next - (std::size_t) start; }
            if(78 == pil++)  { p78 = (std::size_t) next - (std::size_t) start; }
            next = editor->buffer()->next_char(next);
         }
            if(p72)
         {
            std::memset((void*) &buf[p72], 'B', p78 - p72);
         }
         if(len - p78)
         {
            std::memset((void*) &buf[p78], 'C', len - p78);
         }

         style->replace(start, start + (int) len, buf);
      }
      start = editor->buffer()->skip_lines(start, 1);
      std::free((void*) buf);
   }

   // Update editor content
   editor->redisplay_range(pos, end);
}


// =============================================================================
// Compose window Cancel button callback

void  ComposeWindow::cancel_cb_i(Fl_Widget*  w)
{
   // Ignore escape key
   if(Fl::event() == FL_SHORTCUT && Fl::event_key() == FL_Escape)
   {
      return;
   }
   // Destroy compose window
   mainWindow->composeWindow = NULL;
   Fl::delete_widget(w);
   // Update main window state
   mainWindow->composeComplete();
}


// =============================================================================
// Compose window Send button callback
//
// Note: Article is still in local (not canonical) form here

void  ComposeWindow::send_cb_i(void)
{
   const char*  fqdn = config[CONF_FQDN].val.s;
   char*  p;
   std::size_t  len;
   std::size_t  i;
   std::size_t  lenh;
   std::size_t  lenb;
   int  rv = 0;
   const char*  q;
   const char*  newsgroups;
   const char*  subject;
   const char*  organization;
   const char*  msgid;
   const char*  cancel_lock1;
   const char*  cancel_lock;
   const char*  date;
   const char*  injection_date;
   const char*  header;
   const char*  body;
   const char*  expires;
   bool  free_subject = false;
   int  start;
   int  end;
   bool  fup2_present = false;

   // Replace header field "Newsgroups" with potentially modified data
   newsgroups = newsgroupsField->value();
   len = std::strlen(newsgroups);
   if(!len)
   {
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s", S("Newsgroups list is empty"));
      rv = -1;
   }
   else
   {
      // Check content according to RFC 5536
      q = newsgroups;
      for(i = 0; i < len; ++i)
      {
         if(',' == q[i] || '.' == q[i]
            || '+' == q[i] || '-' == q[i] || '_' == q[i])
         {
            continue;
         }
         if(0x30 <= q[i] && 0x39 >= q[i])  { continue; }
         if(0x41 <= q[i] && 0x5A >= q[i])  { continue; }
         if(0x61 <= q[i] && 0x7A >= q[i])  { continue; }
         rv = -1;
         break;
      }
      if(',' == q[0] || ',' == q[len - (std::size_t) 1])  { rv = -1; }
      if('.' == q[0] || '.' == q[len - (std::size_t) 1])  { rv = -1; }
      if(rv)
      {
         SC("Do not use non-ASCII for the translation of this item")
         fl_message_title(S("Error"));
         fl_alert("%s", S("Invalid content in Newsgroups header field"));
      }
      else
      {
         rv = replaceHeaderField("Newsgroups", newsgroups);
         if(rv)
         {
            SC("Do not use non-ASCII for the translation of this item")
            fl_message_title(S("Error"));
            fl_alert("%s", S("Replacement of Newsgroups in header failed"));
            rv = -1;
         }
      }
   }

   // Replace header field "Subject" with potentially modified data
   if(!rv)
   {
      subject = subjectField->value();
      if(!std::strlen(subject))
      {
         SC("Do not use non-ASCII for the translation of this item")
         fl_message_title(S("Error"));
         fl_alert("%s", S("Subject is empty"));
         rv = -1;
      }
      else
      {
         rv = enc_mime_word_encode(&q, subject, std::strlen("Subject: "));
         if(0 <= rv)
         {
            if(!rv)  { free_subject = true; }
            subject = q;
            rv = replaceHeaderField("Subject", subject);
            if(rv)
            {
               SC("Do not use non-ASCII for the translation of this item")
               fl_message_title(S("Error"));
               fl_alert("%s", S("Replacement of Subject in header failed"));
               rv = -1;
            }
            if(free_subject)  { enc_free((void*) subject); }
         }
      }
   }

   // Encode optional header field "Organization" that may be potentially present
   if(!rv)
   {
      organization = extractHeaderField("Organization");
      if(NULL != organization)
      {
         // Verify UTF-8 encoding
         rv = enc_uc_check_utf8(organization);
         if(!rv)
         {
            rv = enc_mime_word_encode(&q, organization,
                                      std::strlen("Organization: "));
            if(0 < rv)  { rv = 0; }
            else if(!rv)
            {
               rv = replaceHeaderField("Organization", q);
               enc_free((void*) q);
            }
         }
         std::free((void*) organization);
         if(rv)
         {
            SC("Do not use non-ASCII for the translation of this item")
            fl_message_title(S("Error"));
            fl_alert("%s", S("Encoding of Organization header field failed"));
            rv = -1;
         }
      }
   }

   // Insert, replace or remove optional header fields before MIME declaration
   if(!rv)
   {
      rv = searchHeaderField("MIME-Version", &start, &end);
      if(rv)
      {
         SC("Do not use non-ASCII for the translation of this item")
         fl_message_title(S("Error"));
         fl_alert("%s", S("Bug: Mandatory MIME declaration not found"));
         rv = -1;
      }
      else
      {
         // Archive
         if(!archiveButton->value())
         {
            if(replaceHeaderField("Archive", "no"))
            {
               compHeader->insert(start, "Archive: no\n");
            }
         }
         else  { deleteHeaderField("Archive"); }
         // Distribution
         q = distriField->value();
         if(NULL != q)
         {
            len = std::strlen(q);
            if(len)
            {
               // Check content (incomplete)
               for(i = 0; i < len; ++i)
               {
                  if(',' == q[i] || '+' == q[i] || '-' == q[i] || '_' == q[i])
                  {
                     continue;
                  }
                  if(0x30 <= q[i] && 0x39 >= q[i])  { continue; }
                  if(0x41 <= q[i] && 0x5A >= q[i])  { continue; }
                  if(0x61 <= q[i] && 0x7A >= q[i])  { continue; }
                  rv = -1;
                  break;
               }
               if(',' == q[0] || ',' == q[len - (std::size_t) 1])  { rv = -1; }
               // RFC 5536 forbids use of "all" and recommends to not use
               // "world" (because this is the default behaviour, the header
               // field is not created in this case)
               p = (char*) std::malloc(len + (std::size_t) 1);
               if(NULL == p)  { rv = -1; }
               else
               {
                  // Convert content to lower case
                  for(i = 0; i < len; ++i)
                  {
                     // Note: Behaviour is undefined if argument is not
                     // representable as unsigned char or is not equal to EOF.
#if defined(__cplusplus) && __cplusplus >= 199711L
                     p[i] = (char) std::tolower((int) (unsigned char) q[i]);
#else  // defined(__cplusplus) && __cplusplus >= 199711L
                     // Pre-C++98 compilers may have problems with the namespace
                     p[i] = (char) tolower((int) (unsigned char) q[i]);
#endif  // defined(__cplusplus) && __cplusplus >= 199711L
                  }
                  p[len] = 0;
                  // Check for "all"
                  // Attention:
                  // Overloaded and both prototypes different than in C!
                  if(NULL != std::strstr(p, "all"))  { rv = -1; }
               }
               // Check for error
               if(rv)
               {
                  SC("Do not use non-ASCII for the translation of this item")
                  fl_message_title(S("Error"));
                  fl_alert("%s",
                            S("Invalid content in Distribution header field"));
               }
               // Check for "world" (omit header field if found)
               // Attention: Overloaded and both prototypes different than in C!
               else if(NULL == std::strstr(p, "world"))
               {
                  if(replaceHeaderField("Distribution", p))
                  {
                     compHeader->insert(start, "\n");
                     compHeader->insert(start, p);
                     compHeader->insert(start, "Distribution: ");
                  }
               }
               else  { deleteHeaderField("Distribution"); }
               std::free((void*) p);
            }
         }
         else  { deleteHeaderField("Distribution"); }
         // Expires
         if(!rv)
         {
            expires = expireField->value();
            if(NULL != expires)
            {
               if(std::strlen(expires))
               {
                  if(enc_convert_iso8601_to_timestamp(&q, expires))
                  {
                     SC("Do not use non-ASCII for the translation of this item")
                     fl_message_title(S("Error"));
                     fl_alert("%s", S("Invalid date for Expires header field"));
                     rv = -1;
                  }
                  else
                  {
                     if(replaceHeaderField("Expires", q))
                     {
                        compHeader->insert(start, "\n");
                        compHeader->insert(start, q);
                        compHeader->insert(start, "Expires: ");
                     }
                     std::free((void*) q);
                  }
               }
            }
            else  { deleteHeaderField("Expires"); }
         }
         // Keywords
         if(!rv)
         {
            q = keywordField->value();
            if(NULL != q)
            {
               len = std::strlen(q);
               if(len)
               {
                  // Check content
                  for(i = 0; i < len; ++i)
                  {
                     // Accept a comma separated list of phrase subsets
                     if(',' == q[i] || '!' == q[i] || '#' == q[i] || '$' == q[i]
                         || '%' == q[i] || '&' == q[i] || '+' == q[i]
                         || '-' == q[i] || '/' == q[i] || '=' == q[i]
                         || '?' == q[i] || '^' == q[i] || '_' == q[i]
                         || '{' == q[i] || '|' == q[i] || '}' == q[i]
                         || '~' == q[i] || ' ' == q[i])
                     {
                        continue;
                     }
                     if(0x30 <= q[i] && 0x39 >= q[i])  { continue; }  // 0-9
                     if(0x41 <= q[i] && 0x5A >= q[i])  { continue; }  // A-Z
                     if(0x61 <= q[i] && 0x7A >= q[i])  { continue; }  // a-z
                     rv = -1;
                     break;
                  }
                  // Reject comma or space as first and last character
                  --len;
                  if(',' == q[0] || ',' == q[len]
                     || ' ' == q[0] || ' ' == q[len])
                  {
                     rv = -1;
                  }
                  if(rv)
                  {
                     SC("Do not use non-ASCII for the translation of this item")
                     fl_message_title(S("Error"));
                     fl_alert("%s",
                              S("Invalid content in Keywords header field"));
                  }
                  else
                  {
                     if(replaceHeaderField("Keywords", q))
                     {
                        compHeader->insert(start, "\n");
                        compHeader->insert(start, q);
                        compHeader->insert(start, "Keywords: ");
                     }
                  }
               }
            }
            else  { deleteHeaderField("Keywords"); }
         }
         // Followup-To
         if(!rv)
         {
            q = fup2Field->value();
            if(NULL != q)
            {
               len = std::strlen(q);
               if(len)
               {
                  // Check content according to RFC 5536
                  for(i = 0; i < len; ++i)
                  {
                     if(',' == q[i] || '.' == q[i]
                        || '+' == q[i] || '-' == q[i] || '_' == q[i])
                     {
                        continue;
                     }
                     if(0x30 <= q[i] && 0x39 >= q[i])  { continue; }
                     if(0x41 <= q[i] && 0x5A >= q[i])  { continue; }
                     if(0x61 <= q[i] && 0x7A >= q[i])  { continue; }
                     rv = -1;
                     break;
                  }
                  if(',' == q[0] || ',' == q[len - (std::size_t) 1])
                  {
                     rv = -1;
                  }
                  if('.' == q[0] || '.' == q[len - (std::size_t) 1])
                  {
                     rv = -1;
                  }
                  if(rv)
                  {
                     SC("Do not use non-ASCII for the translation of this item")
                     fl_message_title(S("Error"));
                     fl_alert("%s",
                              S("Invalid content in Followup-To header field"));
                  }
                  else
                  {
                     if(replaceHeaderField("Followup-To", q))
                     {
                        compHeader->insert(start, "\n");
                        compHeader->insert(start, q);
                        compHeader->insert(start, "Followup-To: ");
                        fup2_present = true;
                     }
                  }
               }
            }
            else  { deleteHeaderField("Followup-To"); }
         }
      }
   }

   // Generate new "Message-ID" header field for every posting attempt
   if(!rv)
   {
      msgid = extractHeaderField("Message-ID");
      if(NULL != msgid)
      {
         std::free((void*) msgid);
         if(std::strlen(fqdn))
         {
            msgid = core_get_msgid(fqdn);
            if(NULL != msgid)
            {
               replaceHeaderField("Message-ID", msgid);
               // Because the Cancel-Keys are based on the Message-ID,
               // a potential Cancel-Lock header field must be regenerated
               rv = searchHeaderField("Cancel-Lock", &start, &end);
               if(rv)  { rv = 0; }
               else
               {
                  deleteHeaderField("Cancel-Lock");
                  // Create new Cancel-Lock header field
                  cancel_lock1 = core_get_cancel_lock(CORE_CL_SHA1, msgid);
                  cancel_lock = core_get_cancel_lock(CORE_CL_SHA256, msgid);
                  if(NULL != cancel_lock1 || NULL != cancel_lock)
                  {
                     compHeader->insert(start, "\n");
                     if(NULL != cancel_lock)
                     {
                        compHeader->insert(start, cancel_lock);
                        compHeader->insert(start, " ");
                     }
                     if(NULL != cancel_lock1)
                     {
                        compHeader->insert(start, cancel_lock1);
                        compHeader->insert(start, " ");
                     }
                     compHeader->insert(start, "Cancel-Lock:");
                     core_free((void*) cancel_lock);
                     core_free((void*) cancel_lock1);
                  }
               }
               core_free((void*) msgid);
            }
         }
      }
   }

   // Update header field "Date" (if already present)
   if(!rv)
   {
      date = extractHeaderField("Date");
      if(NULL != date)
      {
         std::free((void*) date);
         date = core_get_datetime(0);
         rv = replaceHeaderField("Date", date);
         core_free((void*) date);
         if(rv)
         {
            SC("Do not use non-ASCII for the translation of this item")
            fl_message_title(S("Error"));
            fl_alert("%s", S("Updating Date header field failed"));
            rv = -1;
         }
      }
   }

   // Update header field "Injection-Date" (if already present)
   if(!rv)
   {
      injection_date = extractHeaderField("Injection-Date");
      if(NULL != injection_date)
      {
         std::free((void*) injection_date);
         injection_date = core_get_datetime(1);
         rv = replaceHeaderField("Injection-Date", injection_date);
         core_free((void*) injection_date);
         if(rv)
         {
            SC("Do not use non-ASCII for the translation of this item")
            fl_message_title(S("Error"));
            fl_alert("%s", S("Updating Injection-Date header field failed"));
            rv = -1;
         }
      }
   }

   // Check for Xpost (and display warning if no Fup2 is present)
   if(!rv)
   {
      // Attention: Overloaded and both prototypes different than in C!
      if(std::strchr(newsgroupsField->value(), (int) ',') && !fup2_present)
      {
         SC("Do not use non-ASCII for the translation of this item")
         fl_message_title(S("Warning"));
         rv = !fl_choice("%s", S("Cancel"),
                         S("OK"), NULL,
                         S("Xpost without Followup-To\nReally continue?"));
      }
   }

   // Combine header and body
   if(!rv)
   {
      header = compHeader->text();
      body = compText->text();
      if(NULL == header || NULL == body)
      {
         SC("Do not use non-ASCII for the translation of this item")
         fl_message_title(S("Error"));
         fl_alert("%s", S("Out of memory"));
      }
      else
      {
         lenh = std::strlen(header);
         lenb = std::strlen(body);
         // Check for empty body
         if(!lenb)
         {
            SC("Do not use non-ASCII for the translation of this item")
            fl_message_title(S("Error"));
            fl_alert("%s", S("Message body is empty"));
         }
         else
         {
            // Check body content to contain only citations
            if(checkArticleBody(body))
            {
               SC("Do not use non-ASCII for the translation of this item")
               fl_message_title(S("Error"));
               rv = !fl_choice("%s", S("Cancel"),
                               S("OK"), NULL,
                               S("Message body contains no own content"));
            }
            if(!rv)
            {
               // Post article
               p = (char*) std::malloc(lenh + lenb + (std::size_t) 1);
               if(NULL != p)
               {
                  std::strcpy(p, header);
                  std::strcat(p, body);
                  // 'articlePost' will take responsibility for the memory
                  mainWindow->articlePost(UI_CB_START, p);
               }
            }
         }
         std::free((void*) header);
         std::free((void*) body);
         // For code review: Do not 'free()' memory pointed to by 'p' here!
      }
   }
}


// =============================================================================
// URI insertion callback

void  ComposeWindow::uri_insert_cb_i()
{
   const char*  scheme = uriSchemeField->value();
   enum enc_uri_scheme  sch = ENC_URI_SCHEME_INVALID;
   const char*  body_raw = uriBodyField->value();
   const char*  body = NULL;

   // Select scheme
   if(std::strlen(scheme))
   {
      if(!std::strncmp(scheme, "http", 4))
      {
         sch = ENC_URI_SCHEME_HTTP;
      }
      else if(!std::strncmp(scheme, "ftp", 3))
      {
         sch = ENC_URI_SCHEME_FTP;
      }
      else if(!std::strncmp(scheme, "news", 4))
      {
         sch = ENC_URI_SCHEME_NEWS;
      }
      else if(!std::strncmp(scheme, "mailto", 6))
      {
         sch = ENC_URI_SCHEME_MAILTO;
      }
   }
   // Encode body
   body = enc_uri_percent_encode(body_raw, sch);
   if(NULL == body)
   {
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s", S("Creation of URI failed"));
   }
   else
   {
      // Leading delimiter
      compEditor->insert("<");
      // Scheme
      compEditor->insert(scheme);
      // Colon delimiter
      compEditor->insert(":");
      // Authority designator
      if(ENC_URI_SCHEME_HTTP == sch || ENC_URI_SCHEME_FTP == sch)
      {
         compEditor->insert("//");
      }
      // Content (hier-part)
      compEditor->insert(body);
      uriBodyField->value("");
      // Trailing delimiter
      compEditor->insert(">");
      // Switch to content tab
      compTabs->value(compGroup);
      Fl::focus(compEditor);
   }
   if(body != body_raw)  { enc_free((void*) body); }
}


// =============================================================================
// Compose window constructor

ComposeWindow::ComposeWindow(const char*  label, const char*  header,
   const char*  article, const char*  ca, struct core_article_header*  hdr,
   bool super) : UI_WINDOW_CLASS(795, 395, label)
{
   static const char  label_ch[] = "1-----------------------------------------"
      "-------------------------72->|-78->|";
   Fl_Group*  gp;
   Fl_Box*  bp;
   Fl_Button*  p;
   Fl_Box*  chp;
   int  th = 30;  // Tab hight (including gap)
   int  tg = 5;  // Gap between tabs and cards
   int  y, w, h, gy, gh, bw, sh, hh;
   int  w1, w2, w3, w4, h1, h2, h3;
   const char*  signature;
   const char*  subject;
   const char*  newsgroups;  // Unfolded header field body
   const char**  groups = NULL;  // Array of individual group names
   int  testGrp = 0;
   std::size_t  i;
   unsigned int  signature_warnings = 0;
   const char*  uri_header;
   const char*  distribution = NULL;
   int  dist_msg_flag = 0;
   int  rv;
   // Label strings
   SC("Preserve the spaces at beginning and end")
   const char*  label_content = S("  Content  ");
   const char*  label_uri_encoder = S("  URI encoder  ");
   const char*  label_advanced = S("  Advanced  ");
   SC("Preserve the colon")
   const char*  label_subject = S("Subject:");
   const char*  label_change = S("Change");
   const char*  label_send = S("Send");
   const char*  label_cancel = S("Cancel");
   const char*  label_insert = S("Insert");

   // Configure article compose window
   copy_label(label);
   callback(cancel_cb, (void*) this);

   // Create text buffers
   compHeader = new Fl_Text_Buffer();
   compHeader->text(header);
   currentStyle = new Fl_Text_Buffer();
   compText = new Fl_Text_Buffer();
   compText->add_modify_callback(style_update_cb, (void*) this);

   // Add widgets --------------------------------------------------------------
   begin();

   // Create widget group
   compTabs = new Fl_Tabs(0, 0, 795, 395);
   compTabs->begin();
   {
      compGroup = new Fl_Group(0, th - tg, 795, 395 - th + tg, label_content);
      compGroup->begin();
      {
         // Calculate required widths and heights for buttons
         gui_set_default_font();
         fl_measure(label_send, w1 = 0, h1 = 0);
         fl_measure(label_cancel, w2 = 0, h2 = 0);
         if(w1 > w2)  { w = w1; }  else  { w = w2; }
         if(h1 > h2)  { h = h1; }  else  { h = h2; }
         w += 30;
         h += 10;
         gh = h + 10;
         gy = 395 - th - gh;
         // Add subject line
         sh = 30;
         subjectGroup = new Fl_Group(0, th, 795, sh);
         subjectGroup->begin();
         {
            gui_set_default_font();
            fl_measure(label_subject, w3 = 0, h3 = 0);
            w3 += 15;
            fl_measure(label_change, w4 = 0, h3 = 0);
            w4 += 30;
            subjectField = new Fl_Input(w3, th, 795 - w3 - w4, sh,
                                        label_subject);
            subjectField->align(FL_ALIGN_LEFT);
            subject = extractHeaderField("Subject");
            if(NULL != subject)
            {
               subjectField->value(subject);
               std::free((void*) subject);
            }
            p = new Fl_Button(795 - w4, th, w4, sh, label_change);
            p->tooltip(
            S("Change subject and cite old one with was: prefix in parenthesis")
            );
            p->callback(change_cb, (void*) this);
         }
         subjectGroup->end();
         subjectGroup->resizable(subjectField);
         // Add column hints
         hh = sh;
         chp = new Fl_Box(0, th + sh, 795, hh, label_ch);
         chp->labelfont(FL_COURIER);
         chp->align(FL_ALIGN_INSIDE | FL_ALIGN_LEFT);
         // Add text editor field
         compEditor = new Fl_Text_Editor(0, th + sh + hh, 795,
                                         395 - th - sh - hh - gh);
         compEditor->textfont(FL_COURIER);
         compEditor->show_insert_position();
         resizable(compEditor);
         // Create content and style buffers
         styles = new Fl_Text_Display::Style_Table_Entry[3];
         styles[0].color = FL_FOREGROUND_COLOR;
         styles[0].font = FL_COURIER;
         styles[0].size = compEditor->textsize();
         styles[0].attr = 0;
         styles[1].color = FL_COLOR_CUBE + (Fl_Color) 35;
         styles[1].font = FL_COURIER;
         styles[1].size = compEditor->textsize();
         styles[1].attr = 0;
         styles[2].color = FL_RED;
         styles[2].font = FL_COURIER;
         styles[2].size = compEditor->textsize();
         styles[2].attr = 0;
         compEditor->highlight_data(currentStyle, styles, 3, 'A', NULL, NULL);
         // Insert content
         compEditor->buffer(compText);
         if(NULL != article)
         {
            compText->insert(0, article);
            // Cite article if not superseding it
            if(!super)
            {
               if (NULL != ca)  { gui_cite_content(compText, ca, hdr->groups); }
            }
            gui_check_article(compText);
         }
         if(super)  { compEditor->insert_position(0); }
         else  { compEditor->insert_position(compText->length()); }
         // Append signature (if available)
         signature = core_get_signature(&signature_warnings);
         if(NULL != signature)
         {
            if(!(signature_warnings & CORE_SIG_FLAG_INVALID))
            {
               if(!super)  { compEditor->buffer()->append("\n"); }
               if(signature_warnings & CORE_SIG_FLAG_SEPARATOR)
               {
                  compEditor->buffer()->append("-- \n");
               }
               compEditor->buffer()->append(signature);
            }
            core_free((void*) signature);
         }
         // Add button bar at bottom
         gp = new Fl_Group(0, th + gy, 795, gh);
         gp->begin();
         {
            bw = w + 30;
            y = th + gy + 5;
            new Fl_Box(0, th + gy, bw, gh);
            p = new Fl_Button(15, y, w, h, label_send);
            p->callback(send_cb, (void*) this);
            new Fl_Box(795 - bw, gy, bw, gh);
            p = new Fl_Button(795 - bw + 15, y, w, h, label_cancel);
            p->callback(cancel_cb, (void*) this);
            // Resizable space between buttons
            bp = new Fl_Box(bw, th + gy, 795 - (2 * bw), gh);
         }
         gp->end();
         gp->resizable(bp);
      }
      compGroup->end();
      compGroup->resizable(compEditor);

      // -----------------------------------------------------------------------

      uriEncGroup = new Fl_Group(0, th - tg, 795, 395 - th + tg,
                                 label_uri_encoder);
      uriEncGroup->begin();
      {
         y = th + tg + 10;
         uri_header = S("Percent encoder for clickable URIs");
         uriHeaderField = new Fl_Box(5, y, 795 - 10, sh, uri_header);
         uriHeaderField->labelfont(FL_HELVETICA_BOLD);
         y += 2 * sh;
         uriSchemeField = new Fl_Input_Choice(5, y, 795 - 10, sh,
                                              S("URI scheme:"));
         uriSchemeField->tooltip(S("Only the selectable schemes are supported"));
         uriSchemeField->align(FL_ALIGN_TOP_LEFT);
         uriSchemeField->add("http");
         uriSchemeField->add("https");
         uriSchemeField->add("ftp");
         uriSchemeField->add("news");
         uriSchemeField->add("mailto");
         uriSchemeField->input()->readonly(1);
         uriSchemeField->value(0);  // Default to first entry in option list
         y += 2 * sh;
         uriBodyField = new Fl_Input(5, y, 795 - 10, sh, S("Body for URI:"));
         uriBodyField->align(FL_ALIGN_TOP_LEFT);
         // Add insert button at bottom
         y += 2 * sh;
         fl_measure(label_insert, w1 = 0, h1 = 0);
         w = w1 + 30;
         h = h1 + 10;
         gp = new Fl_Group(0, y, 795, h);
         gp->begin();
         {
            p = new Fl_Button(5, y, w, h, label_insert);
            p->callback(uri_insert_cb, (void*) this);
            fillSpace = new Fl_Box(5 + w, y, 795 - (10 + w), h);
         }
         gp->end();
         gp->resizable(fillSpace);
         // Fill unused space below button
         y += sh + 5;
         fillSpace = new Fl_Box(5, y, 795 - 10, 1);
      }
      uriEncGroup->end();
      uriEncGroup->resizable(fillSpace);

      // -----------------------------------------------------------------------

      advancedGroup = new Fl_Group(0, th - tg, 795, 395 - th + tg,
                                   label_advanced);
      advancedGroup->begin();
      {
         y = th + tg + 20;
         newsgroupsField = new Fl_Input(5, y, 795 - 10, sh, "Newsgroups:");
         newsgroupsField->align(FL_ALIGN_TOP_LEFT);
         newsgroupsField->tooltip(S("Comma separated list"));
         newsgroups = extractHeaderField("Newsgroups");
         if(NULL != newsgroups)
         {
            newsgroupsField->value(newsgroups);
            // Split body into group array and check for test groups
            groups = core_extract_groups(newsgroups);
            if(NULL != groups)
            {
               // Check group array (and destroy it again)
               i = 0;
               while(NULL != groups[i])
               {
                  if(!testGrp)  { testGrp = filter_check_testgroup(groups[i]); }
                  core_free((void*) groups[i++]);
               }
               core_free((void*) groups);
            }
            // Release memory for header field body
            std::free((void*) newsgroups);
         }
         y += 2 * sh;
         fup2Field = new Fl_Input_Choice(5, y, 795 - 10, sh, "Followup-To:");
         fup2Field->align(FL_ALIGN_TOP_LEFT);
         fup2Field->tooltip(S("Comma separated list"));
         fup2Field->add("poster");
         if(NULL != hdr)
         {
            for(i = 0; i < UI_XPOST_LIMIT; ++i)
            {
               if(NULL == hdr->groups[i])  { break; }
               else
               {
                  // Check for Xpost
                  if(!i && NULL == hdr->groups[1])  { break; }
                  fup2Field->add(hdr->groups[i]);
               }
            }
            if(super)
            {
               if(NULL != hdr->fup2 && std::strlen(hdr->fup2))
               {
                  fup2Field->value(hdr->fup2);
               }
            }
         }
         y += 2 * sh;
         keywordField = new Fl_Input_Choice(5, y, 795 - 10, sh, "Keywords:");
         keywordField->align(FL_ALIGN_TOP_LEFT);
         keywordField->tooltip(S("Comma separated list"));
         if(!testGrp)  { keywordField->add("ignore"); }
         else
         {
            // Set keywords for test group
            std::printf("%s: %s"
                        "Setting keywords from config file for test group\n",
                        CFG_NAME, MAIN_ERR_PREFIX);
            keywordField->value(config[CONF_TESTGRP_KWORDS].val.s);
         }
         y += 2 * sh;
         expireField = new Fl_Input(5, y, 795 - 10, sh, "Expires:");
         expireField->align(FL_ALIGN_TOP_LEFT);
         expireField->tooltip(
            S("Use date in ISO 8601 format YYYY-MM-DD")
         );
         y += 2 * sh;
         distriField = new Fl_Input_Choice(5, y, 795 - 10, sh, "Distribution:");
         distriField->align(FL_ALIGN_TOP_LEFT);
         if(NULL != hdr && NULL != hdr->dist && std::strlen(hdr->dist))
         {
            // Follow distribution for follow-up
            if(!super)
            {
               std::printf("%s: %sFollowing former distribution\n",
                           CFG_NAME, MAIN_ERR_PREFIX);
            }
            distriField->value(hdr->dist);
         }
         else
         {
            // Follow suggestion of server
            rv = -1;
            newsgroups = extractHeaderField("Newsgroups");
            if(NULL != newsgroups)
            {
               // Split newsgroup field body into group array
               groups = core_extract_groups(newsgroups);
               // Match groups against distribution patterns
               rv = core_get_distribution(&distribution, groups);
               if(NULL != groups)
               {
                  // Destroy group array again
                  i = 0;
                  while(NULL != groups[i])  { core_free((void*) groups[i++]); }
                  core_free((void*) groups);
               }
               // Release memory for header field body
               std::free((void*) newsgroups);
            }
            if(!rv && config[CONF_DIST_SUGG].val.i)
            {
               dist_msg_flag = 1;
               std::printf("%s: %sSetting distribution as suggested\n",
                           CFG_NAME, MAIN_ERR_PREFIX);
               distriField->value(distribution);
               core_free((void*) distribution);
            }
            else  { distriField->value("world"); }
         }
         distriField->add("world");
         distriField->add("local");
         distriField->tooltip(
            S("Use country code or comma separated list of country codes")
         );
         y += sh + 5;
         archiveButton = new Fl_Check_Button(5, y, 795 - 10, sh, "Archive");
         archiveButton->set();
         // Fill unused space after resize
         y += sh + 5;
         fillSpace = new Fl_Box(5, y, 795 - 10, 1);
      }
      advancedGroup->end();
      advancedGroup->resizable(fillSpace);
   }
   compTabs->end();
   compTabs->resizable(compGroup);

   end();
   // --------------------------------------------------------------------------

   // Set focus
   if(!std::strlen(subjectField->value()))  { Fl::focus(subjectField); }
   else  { Fl::focus(compEditor); }
   // Set minimum window size
   size_range(4 * bw, 395, 0, 0);

   // Display potential message for distribution
   if(dist_msg_flag)
   {
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Note"));
      fl_message("%s", S("Distribution set as suggested by server"));
   }

   // Display potential warnings for signature
   if(signature_warnings & CORE_SIG_FLAG_INVALID)
   {
      SC("Do not use non-ASCII for the translation of this item")
      fl_message_title(S("Error"));
      fl_alert("%s", S("Signature has unsupported character set"));
   }
   else
   {
      if(signature_warnings & CORE_SIG_FLAG_LENGTH)
      {
         SC("Do not use non-ASCII for the translation of this item")
         fl_message_title(S("Note"));
         fl_message("%s", S("Signature should not be longer than 4 lines"));
      }
   }

   // Check for external editor
   if(!std::strlen(config[CONF_EDITOR].val.s))
   {
#if !CFG_DB_DISABLE
      Fl::visual(FL_DOUBLE | FL_INDEX);
#endif  // CFG_DB_DISABLE
      show();
   }
   else
   {
      // Since FLTK 1.3.3 it is required to explicitly hide the new window
      hide();
      mainWindow->composeWindowLock = 1;
      UI_STATUS(S("Waiting for external editor ..."));
   }
}


// =============================================================================
// Compose window destructor

ComposeWindow::~ComposeWindow(void)
{
   // Detach text buffers and destroy them
   compEditor->highlight_data(dummyTb, NULL, 0, 'A', NULL, NULL);
   delete currentStyle;
   delete[] styles;
   compEditor->buffer(dummyTb);
   delete compText;
   delete compHeader;
}


// =============================================================================
//! \brief Init GUI
//!
//! \attention
//! Remember that the locale must use either UTF-8 or ISO 8859-1 codeset or be
//! the POSIX locale.
//!
//! \param[in] argc  Command line argument count
//! \param[in] argv  Pointer to Command line argument array

void  ui_init(int  argc, char**  argv)
{
   int  rv;
   int  w = 730;
   int  h = 350;
   int  x = 1;
   int  y = 1;
   int  tx = 230;
   int  ty = 140;
   const char*  text;
   Fl_Text_Buffer*  info;
   const char*  pass = NULL;
   const char*  title_psfile;

   // Initialize MT support of FLTK
   rv = Fl::lock();
   if(rv)
   {
      PRINT_ERROR("No thread support available in FLTK");
      exitRequest = 1;
   }
   else
   {
      // This flag must be set after MT support of FLTK was initialized,
      // but before spawning any additional threads!
      lockingInitialized = true;
      // Start core (this will spawn a second thread)
      rv = core_init();
      if(rv)  { exitRequest = 1; }
   }
   if(!exitRequest)
   {
      // Set global button labels
      SC("")
      SC("This section is for the button labels of popup windows")
      fl_cancel = S("Cancel");
      fl_close = S("Close");
      fl_ok = S("OK");
      fl_yes = S("Yes");
      fl_no = S("No");
      SC("")

      // Set global labels for text buffer
      SC("")
      SC("This section is for the text buffer warning messages")
      Fl_Text_Buffer::file_encoding_warning_message
         = S("Invalid encoding detected, trying to convert data");

      // Set global labels for file chooser
      SC("")
      SC("This section is for the file chooser labels")
      Fl_File_Chooser::all_files_label = S("*");
      Fl_File_Chooser::custom_filter_label = S("Custom filter");
      Fl_File_Chooser::existing_file_label
         = S("Please choose an existing file!");
      Fl_File_Chooser::favorites_label = S("Favorites");
      Fl_File_Chooser::add_favorites_label = S("Add to favorites");
      Fl_File_Chooser::manage_favorites_label = S("Manage favorites");
      Fl_File_Chooser::filesystems_label = S("File systems");
      Fl_File_Chooser::new_directory_label = S("New directory?");
      Fl_File_Chooser::new_directory_tooltip = S("Create a new directory.");
      Fl_File_Chooser::preview_label = S("Preview");
      Fl_File_Chooser::hidden_label = S("Show hidden files");
      Fl_File_Chooser::filename_label = S("Filename:");
      Fl_File_Chooser::save_label = S("Save");
      SC("The translation for this should not be much longer!")
      Fl_File_Chooser::show_label = S("Show:");
      SC("")

      // Set global labels for print dialog
      SC("")
      SC("This section is for the print dialog labels")
      SC("Do not use characters for the translation that cannot be")
      SC("converted to the ISO 8859-1 character set for this item.")
      SC("Leave the original string in place if in doubt.")
      Fl_Printer::dialog_title = S("Print");
      Fl_Printer::dialog_printer = S("Printer");
      Fl_Printer::dialog_range = S("Print range");
      Fl_Printer::dialog_copies = S("Copies");
      Fl_Printer::dialog_all = S("All");
      Fl_Printer::dialog_pages = S("Pages");
      Fl_Printer::dialog_from = S("From:");
      Fl_Printer::dialog_to = S("To:");
      Fl_Printer::dialog_properties = S("Properties ...");
      Fl_Printer::dialog_copyNo = S("Copies:");
      Fl_Printer::dialog_print_button = S("Print");
      Fl_Printer::dialog_cancel_button = S("Cancel");
      Fl_Printer::dialog_print_to_file = S("Print to file");
      SC("")

      // Set global labels for printer property dialog
      SC("")
      SC("This section is for the printer property dialog labels")
      SC("Do not use characters for the translation that cannot be")
      SC("converted to the ISO 8859-1 character set for this item.")
      SC("Leave the original string in place if in doubt.")
      Fl_Printer::property_title = S("Printer properties");
      Fl_Printer::property_pagesize = S("Page size:");
      Fl_Printer::property_mode = S("Output mode:");
      Fl_Printer::property_use = S("Use");
      Fl_Printer::property_save = S("Save");
      Fl_Printer::property_cancel = S("Cancel");
      SC("")

      // Set global label for print to file chooser dialog
      SC("")
      SC("This section is for the print to file chooser dialog label")
      SC("Do not use characters for the translation that cannot be")
      SC("converted to the ISO 8859-1 character set for this item.")
      SC("Leave the original string in place if in doubt.")
      title_psfile = S("Select a .ps file");
      SC("")
      // Following the FLTK documentation this should always work
      Fl_PostScript_File_Device::file_chooser_title = title_psfile;

      // Check clamp article count value
      if(UI_CAC_MIN > config[CONF_CAC].val.i)
      {
         config[CONF_CAC].val.i = UI_CAC_MIN;
      }
      if(UI_CAC_MAX < config[CONF_CAC].val.i)
      {
         config[CONF_CAC].val.i = UI_CAC_MAX;
      }

#if CFG_CMPR_DISABLE
      // Disable compression negotiation if compiled without support
      config[CONF_COMPRESSION].val.i = 0;
#endif  // CFG_CMPR_DISABLE

      // Construct main window
      dummyTb = new Fl_Text_Buffer(0, 0);
      mainWindow = new MainWindow(CFG_NAME);

      // Restore last window position and size
      mainWindow->size_range(w, h, 0, 0);
      if(config[CONF_POS_X].val.i >= x)  { x = config[CONF_POS_X].val.i; }
      if(config[CONF_POS_Y].val.i >= y)  { y = config[CONF_POS_Y].val.i; }
      if(config[CONF_SIZE_X].val.i >= w)  { w = config[CONF_SIZE_X].val.i; }
      if(config[CONF_SIZE_Y].val.i >= h)  { h = config[CONF_SIZE_Y].val.i; }
      mainWindow->resize(x, y, w, h);
      // Restore tiling
      tx = config[CONF_TILE_X].val.i;
      ty = config[CONF_TILE_Y].val.i;
      mainWindow->setTilingX(tx);
      mainWindow->setTilingY(ty);
      mainWindow->redraw();

      // Don't move message/choice windows under mouse cursor
      fl_message_hotspot(0);

#if USE_WINDOW_ICON
      // Set default icon
      mainWindow->default_icon(&mainIcon);
#endif  // USE_WINDOW_ICON

      // Show GUI
      // The command line argument '-display' is parsed here. Ensure that the
      // connection to the X server is not opened before this point!
#if !CFG_DB_DISABLE
      Fl::visual(FL_DOUBLE | FL_INDEX);
#endif  // CFG_DB_DISABLE
      mainWindow->show(argc, argv);

      // =======================================================================

      // Set default font
      gui_set_default_font();

      // Initialize current article
      info = new Fl_Text_Buffer;
      text = gui_greeting();
      info->text(text);
      delete[] text;
      mainWindow->articleUpdate(info);

      // Display warning if TLS module has detected potential vulnerability
      // Note: Do not move the NLS string inside the #if block
      text = S("Possible security vulnerability in TLS module detected!");
#if CFG_USE_TLS
#  if !CFG_TLS_WARNING_DISABLE
      rv = tls_vulnerability_check(1);
#  else  // !CFG_TLS_WARNING_DISABLE
      rv = tls_vulnerability_check(0);
#  endif  // !CFG_TLS_WARNING_DISABLE
      if(0 > rv)
      {
         SC("Do not use non-ASCII for the translation of this item")
         fl_message_title(S("Warning"));
         fl_alert("%s", text);
      }
#endif  // CFG_USE_TLS

      // Check TLS certificate CRL update interval
      // Ask user whether automatic updates should be disabled if elapsed
#if CFG_USE_TLS
      rv = tls_crl_update_check();
      if(rv)
      {
         SC("Do not use non-ASCII for the translation of this item")
         fl_message_title(S("Note"));
         rv = fl_choice("%s", S("Skip"),
                        S("OK"), NULL,
                        S("TLS certificate revocation list \x28\x43RL\x29\nupdate interval elapsed. Update now?"));
         if(!rv)
         {
            // Suppress automatic CRL updates for current session
            std::printf("%s: %sTLS certificate CRL updates suppressed\n",
                        CFG_NAME, MAIN_ERR_PREFIX);
            tls_crl_update_control(1);
         }
      }
#endif  // CFG_USE_TLS

      // Ask for password if there is none but authentication is enabled
      if(!config[CONF_PASS].val.s[0])  { conf_ephemeral_passwd = 1; }
      if(1 == config[CONF_AUTH].val.i && conf_ephemeral_passwd)
      {
         do
         {
            pass = fl_password("%s", config[CONF_PASS].val.s, "Password:");
            if(NULL == pass)  { exitRequest = 1;  break; }
            else
            {
               conf_string_replace(&config[CONF_PASS], pass);
               if(!config[CONF_PASS].val.s[0])
               {
                  SC("Do not use non-ASCII for the translation of this item")
                  fl_message_title(S("Warning"));
                 fl_alert("%s", S("Empty password is not supported"));
               }
            }
         }  while(!config[CONF_PASS].val.s[0]);
      }

      // Initialize group tree via group list refresh timeout callback
      MainWindow::group_list_refresh_timer_cb((void*) mainWindow);

      //! \todo
      //! Workaround for "creeping window" problem without session manager seems
      //! not to be the "right" solution.
      //! If you know a better one, please report it.
      // Some window managers reposition the window while adding the borders.
      // First we assign the CPU so that this can happen.
      // Then we read back the new position and calculate correction offsets.
      // The stored correction offsets are used on shutdown to calculate the
      // positions that are saved in configfile.
      // The correction is limited to 20 points for the case that the window
      // manager moves the window to a completely different position.
      Fl::check();
      offset_correction_x = x - mainWindow->x();
      if(20 < offset_correction_x)  { offset_correction_x = 0; }
      offset_correction_y = y - mainWindow->y();
      if(20 < offset_correction_y)  { offset_correction_y = 0; }
   }
}


// =============================================================================
//! \brief Drive GUI
//!
//! Assign the CPU to the GUI to process queued operations.
//! Before this function is called, \ref ui_init() must be executed.
//!
//! \return
//! - 0 on success
//! - 1 to indicate an exit request from the GUI

int  ui_exec(void)
{
   static const char*  tmpfile = NULL;
   static long int  editor_pid;
   static bool  editor_term_lock = false;
   int  res = 0;
   int  rv;
   bool  error;

   // Check whether main window is present
   if(NULL == mainWindow)  { res = 1; }
   else
   {
      // Update protocol console
      if(NULL != protocolConsole) { protocolConsole->update(); }

      // Drive GUI
      Fl::wait(0.5);

      // State machine to handle child process for external editor
      if(NULL == mainWindow->composeWindow)
      {
         mainWindow->composeWindowLock = 0;
      }
      switch(mainWindow->composeWindowLock)
      {
         case 0:
         {
            // Check for exit request
            if(exitRequest)  { res = 1; }
            break;
         }
         case 1:  // Spawn child process for external editor
         {
            error = true;
            tmpfile = core_tmpfile_create();
            if(NULL != tmpfile)
            {
               rv = gui_save_to_file(tmpfile, mainWindow->composeWindow
                                              ->compEditor->buffer());
               if(!rv)
               {
                  rv = ext_editor(tmpfile, 1, &editor_pid);
                  if(!rv)  { error = false; }
               }
               if(error)
               {
                  core_tmpfile_delete(tmpfile);
                  tmpfile = NULL;
               }
            }
            if(error)
            {
               UI_STATUS(S("Starting external editor failed."));
               mainWindow->composeWindowLock = 3;
            }
            else  { mainWindow->composeWindowLock = 2; }
            break;
         }
         case 2:  // Poll state of external editor
         {
            rv = ext_editor_status(editor_pid);
            if(-1 == rv)
            {
               // External editor still running
               if(exitRequest && !editor_term_lock)
               {
                  // Terminate external editor
                  ext_editor_terminate(editor_pid);
                  editor_term_lock = true;
               }
            }
            else
            {
               // External editor child process has terminated
               if(rv)  { UI_STATUS(S("External editor reported error.")); }
               else
               {
                  // Import data from external editor
                  UI_STATUS(S("External editor reported success."));
                  rv = mainWindow->composeWindow
                       ->compEditor->buffer()->loadfile(tmpfile);
                  if(rv)
                  {
                     mainWindow->composeWindow
                     ->compEditor->buffer()
                     ->append(S("[Importing data from editor failed]"));
                  }
               }
               core_tmpfile_delete(tmpfile);
               tmpfile = NULL;
               mainWindow->composeWindowLock = 3;
            }
            break;
         }
         case 3:  // Show compose window
         {
            editor_term_lock = false;
            mainWindow->composeWindow->show();
            mainWindow->composeWindowLock = 0;
            break;
         }
         default:
         {
            PRINT_ERROR("Error in external editor state machine (bug)");
            res = 1;
            break;
         }
      }
   }

   return(res);
}


// =============================================================================
//! \brief Shutdown GUI
//!
//! It is not allowed to call \ref ui_exec() after this function returns.

void  ui_exit(void)
{
   if(NULL != mainWindow)
   {
      // Store current main window size
      config[CONF_POS_X].val.i = mainWindow->x() + offset_correction_x;
      config[CONF_POS_Y].val.i = mainWindow->y() + offset_correction_y;
      config[CONF_SIZE_X].val.i = mainWindow->w();
      config[CONF_SIZE_Y].val.i = mainWindow->h();
      config[CONF_TILE_X].val.i = mainWindow->getTilingX();
      config[CONF_TILE_Y].val.i = mainWindow->getTilingY();

      // Store group states
      if(main_debug)
      {
         printf("%s: %sExport group states for shutdown\n",
                CFG_NAME, MAIN_ERR_PREFIX);
      }
      mainWindow->groupStateExport();
   }

   // Shutdown core (and join with the core thread)
   core_exit();
   // MT lock of FLTK no longer required
   lockingInitialized = false;

   // The main window must exist until the core was shutdown (to be able to catch
   // all remaining callbacks)
   if(NULL != mainWindow)
   {
      // Destroy main window
      // This must be done after core shutdown (joining the core thread),
      // otherwise the core thread may create callbacks to the already destroyed
      // object.
      delete mainWindow;

      // This must be done last because the dummy buffer is required to destroy
      // the main window.
      delete dummyTb;
   }

}


// =============================================================================
//! \brief Check whether locale use UTF-8 encoding
//!
//! Determine which codeset the locale uses that FLTK will set for \c LC_CTYPE
//! after opening the X11 display.
//!
//! \attention
//! This works even if the X11 display is not opened yet! This means the return
//! value is valid before \ref ui_init() is called too.
//!
//! \return
//! - 1 Locale uses UTF-8 encoding
//! - 0 Locale uses other encoding

int  ui_get_locale_utf8(void)
{
   return(fl_utf8locale());
}


// =============================================================================
//! \brief Wakeup callback (called by core thread after operation has finished)
//!
//! \param[in] cookie  Cookie that was assigned to the operation by GUI

void  ui_wakeup(unsigned int  cookie)
{
   int  rv;

   // Schedule the callback indicated by 'cookie' and awake UI thread
   switch(cookie)
   {
      case UI_CB_COOKIE_SERVER:
      {
         rv = Fl::awake(MainWindow::serverconf_cb, (void*) mainWindow);
         break;
      }
      case UI_CB_COOKIE_GROUPLIST:
      {
         rv = Fl::awake(MainWindow::subscribe_cb1, (void*) mainWindow);
         break;
      }
      case UI_CB_COOKIE_GROUPLABELS:
      {
         rv = Fl::awake(MainWindow::subscribe_cb2, (void*) mainWindow);
         break;
      }
      case UI_CB_COOKIE_GROUPPROPOSAL:
      {
         rv = Fl::awake(MainWindow::groupproposal_cb, (void*) mainWindow);
         break;
      }
      case UI_CB_COOKIE_GROUPINFO1:
      {
         rv = Fl::awake(MainWindow::refresh_cb1, (void*) mainWindow);
         break;
      }
      case UI_CB_COOKIE_GROUPINFO2:
      {
         rv = Fl::awake(MainWindow::refresh_cb2, (void*) mainWindow);
         break;
      }
      case UI_CB_COOKIE_GROUP:
      {
         rv = Fl::awake(MainWindow::group_cb, (void*) mainWindow);
         break;
      }
      case UI_CB_COOKIE_OVERVIEW:
      {
         rv = Fl::awake(MainWindow::overview_cb, (void*) mainWindow);
         break;
      }
      case UI_CB_COOKIE_HEADER:
      {
         rv = Fl::awake(MainWindow::header_cb, (void*) mainWindow);
         break;
      }
      case UI_CB_COOKIE_BODY:
      {
         rv = Fl::awake(MainWindow::body_cb, (void*) mainWindow);
         break;
      }
      case UI_CB_COOKIE_MOTD:
      {
         rv = Fl::awake(MainWindow::motd_cb, (void*) mainWindow);
         break;
      }
      case UI_CB_COOKIE_ARTICLE:
      {
         rv = Fl::awake(MainWindow::article_cb, (void*) mainWindow);
         break;
      }
      case UI_CB_COOKIE_SRC:
      {
         rv = Fl::awake(MainWindow::src_cb, (void*) mainWindow);
         break;
      }
      case UI_CB_COOKIE_POST:
      {
         rv = Fl::awake(MainWindow::post_cb, (void*) mainWindow);
         break;
      }
      default:
      {
         PRINT_ERROR("Can't assign cookie to callback function");
         rv = -1;
         break;
      }
   }
   if(rv)
   {
      PRINT_ERROR("Registering awake callback failed (fatal error)");
      exitRequest = 1;
   }
}


// =============================================================================
//! \brief Lock for multithread support
//!
//! This function must be called by other threads before they call an OS
//! function that is not guaranteed to be thread-safe. Prominent examples are:
//! - \c getenv()
//! - \c gethostbyname()
//! - \c localtime()
//! - \c readdir()
//!
//! FLTK documentation for MT locking:
//! <br>
//! https://www.fltk.org/doc-1.3/advanced.html#advanced_multithreading
//!
//! \return
//! - 0 on success
//! - Negative value on error

int  ui_lock(void)
{
   int  res = 0;

   // Only the UI thread exist until locking is initialized
   if(lockingInitialized)
   {
      res = Fl::lock();
      if(res)  { res = -1; }
   }

   return(res);
}


// =============================================================================
//! \brief Unlock for multithread support
//!
//! \return
//! - 0 on success
//! - Negative value on error

int  ui_unlock(void)
{
   // Only the UI thread exist until locking is initialized
   if(lockingInitialized)
   {
      Fl::unlock();
   }

   return(0);
}


//! @}

// EOF
