// =============================================================================
//! \file
//! \brief GUI based on FLTK 1.3
//!
//! Copyright (c) 2012-2016 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).


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

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

// FLTK 1.3
#include <FL/Enumerations.H>
#include <FL/Fl.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Check_Button.H>
#include <FL/Fl_Hold_Browser.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_Pack.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_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>

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


// =============================================================================
//! \addtogroup UI
//!
//! 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 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

//! \brief Default font size in pixels (EXPERIMENTAL)
//!
//! Example for 20 pixels:
//! <br>
//! \#define USE_CUSTOM_FONTSIZE  (Fl_Fontsize) 20
#define USE_CUSTOM_FONTSIZE  FL_NORMAL_SIZE

//! \brief Use window icon (EXPERIMENTAL)
//!
//! \note
//! At least FLTK 1.3.3 is required or patches must be applied to FLTK.
//! See STR #2816 for details:
//! <br>
//! http://www.fltk.org/str.php?L2816
#define USE_WINDOW_ICON  0


// =============================================================================
// Experimental features for FLTK compiled with X11 backend

//! \brief Setting this to 1 enables glyph substitution (EXPERIMENTAL)
//!
//! \attention
//! The V7 patches from Ian MacArthur must be applied to FLTK for this to work
//! and FLTK must be compiled with Xft/fontconfig support.
//! See STR #1903 for details:
//! <br>
//! http://www.fltk.org/str.php?L1903
//!
//! \note
//! This value only takes effekt for the X11 backend of FLTK
#define USE_X11_GLYPH_SUBSTITUTION  0


// =============================================================================
// Features for FLTK compiled with Cocoa backend

//! \brief Setting this to 1 moves the menu bar to top of desktop
//!
//! \note
//! This only works for the Cocoa backend of FLTK
#define USE_COCOA_SYS_MENUBAR  0


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

//! \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_GROUPINFO1  2U
#define UI_CB_COOKIE_GROUPINFO2  3U
#define UI_CB_COOKIE_GROUP       4U
#define UI_CB_COOKIE_OVERVIEW    5U
#define UI_CB_COOKIE_HEADER      6U
#define UI_CB_COOKIE_BODY        7U
#define UI_CB_COOKIE_MOTD        8U
#define UI_CB_COOKIE_SRC         9U
#define UI_CB_COOKIE_POST        10U
// @}

//! \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 RFC4643
// @}

//! \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

// 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


// =============================================================================
// 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;
      }
};


// 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  positions_recalculated;
      Fl_Tree_Item*  current_article;

      int  handle(int);
   public:
      inline void  not_drawn(void)  { positions_recalculated = false; }
      inline bool  drawn(void)  { 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)
      {
         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)  { grpAuth->activate(); }
         else
         {
            scfgAuthOff->setonly();
            grpAuth->deactivate();
         }
      }
      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_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);
};

// 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;

      // 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(!(bool) 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)
      {
         Fl_Tree_Item*  ti;

         ti = subscribeTree->find_item(entry);
         if(NULL == ti)  { ti = subscribeTree->add(entry); }
         if(NULL == ti)
         {
            print_error("UI: 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*) true);
         }
      }
      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);
      ~SubscribeWindow(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 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(Fl_Widget*  w);
      static void  change_cb(Fl_Widget*, void*  w)
      {
         ((ComposeWindow*) w)->change_cb_i((Fl_Widget*) w);
      }

      // Style update callback
      inline void  style_update_cb_i(int  pos, int  nInserted, int  nDeleted,
                                     int  nRestyled, const char*  deletedText,
                                     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)
      {
         ((ComposeWindow*) w)
          ->style_update_cb_i(pos, nInserted, nDeleted, nRestyled, deletedText,
                              ((ComposeWindow*) w)->currentStyle,
                              ((ComposeWindow*) w)->compEditor);

      }

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

      // 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(Fl_Widget*);
      static void  uri_insert_cb(Fl_Widget*, void*  w)
      {
         ((ComposeWindow*) w)->uri_insert_cb_i((Fl_Widget*) w);
      }

      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);
};

// MIME multipart content
class MIMEContent
{
   struct MIMEContentListElement
   {
      enc_mime_cte  cte;
      enc_mime_ct  ct;
      const char*  content;
      MIMEContentListElement*  next;
   };

   private:
      std::size_t  partNum;  // Number of elements in linked list
      MIMEContentListElement*  partList;  // Linked list

      MIMEContentListElement*  decodeElement(const char*, std::size_t, char*,
                                             bool, std::size_t*);
      MIMEContentListElement*  initElement(const char*, std::size_t,
                                           enc_mime_cte, enc_mime_ct*);
   public:
      inline std::size_t  parts(void)  { return(partNum); }
      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;
            *ctp = &mcle->ct;
            return(mcle->content);
         }
      }
      MIMEContent(struct core_article_header*, const char*);
      ~MIMEContent(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_GROUP,
      STATE_SCROLL,
      STATE_NEXT,
      STATE_COMPOSE
   };

   // 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 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 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 USE_COCOA_SYS_MENUBAR
      Fl_Sys_Menu_Bar*  menu;
#else  // USE_COCOA_SYS_MENUBAR
      Fl_Menu_Bar*  menu;
#endif  // USE_COCOA_SYS_MENUBAR
      Fl_Hold_Browser*  groupList;
      core_groupdesc*  subscribedGroups;  // Array of subscribed groups
      core_groupdesc*  currentGroup;  // Descriptor of current group
      My_Tree*  articleTree;
      core_hierarchy_element*  currentArticleHE;
      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;
      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;

      // 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 state array

      // 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)
      {
         ((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();
      }

      // 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();
      }

      // Debug mode callback
      inline void  debug_cb_i(void)
      {
         if(core_debug)
         {
            core_debug = 0;
            ui_debug = 0;
         }
         else
         {
            core_debug = 1;
            ui_debug = 1;
         }
      }
      static void  debug_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->debug_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();

         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
         {
            core_unsubscribe_group(&group_num, &group_list, &group_list_index);
            core_export_group_states(group_num, group_list);
            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;

         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();
         core_anum_t  ur;

         for(i = 0; i < groups; ++i)
         {
            ur = groupGetUnreadNo(subscribedGroups[i].lwm,
                                  subscribedGroups[i].hwm, group_list[i].info);
            if(ur)
            {
               groupList->select(++i);
               groupSelect(UI_CB_START, i);
               break;
            }
         }
      }
      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(); }
      }
      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 as unread callback
      inline void  msau_cb_i(void);
      static void  msau_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->msau_cb_i();
      }

      // Mark all as read callback
      inline void  maar_cb_i(void);
      static void  maar_cb(Fl_Widget*, void*  w)
      {
         ((MainWindow*) w)->maar_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();
      }

      // 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  updateArticleTree(int);
      void  articleSelect(int);
      void  viewMotd(int);
      void  viewSrc(int);
      void  stripAngleAddress(char*);
      void  articleCompose(bool, bool);
      void  sendEmail(void);
      void  hyperlinkHandler(int);
      const char*  printHeaderFields(struct core_article_header*);
      bool  stateMachine(enum mainWindowEvent);
   public:
      static void  serverconf_cb(void*  w)
      {
         ((MainWindow*) w)->updateServer(UI_CB_CONTINUE);
      }
      static void  subscribe_cb(void*  w)
      {
         ((MainWindow*) w)->groupSubscribe(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  src_cb(void*  w)
      {
         ((MainWindow*) w)->viewSrc(UI_CB_CONTINUE);
      }
      static void  post_cb(void*  w)
      {
         ((MainWindow*) w)->articlePost(UI_CB_CONTINUE, NULL);
      }
      void  ascrolldown_cb(bool);
      void  articlePost(int, const char*);
      inline void  composeComplete(void)
      {
         if(!stateMachine(EVENT_COMPOSE_EXIT))
         {
            print_error("UI: 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->position(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->position(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

//! Enable additional debug output if nonzero
int  ui_debug = 0;

// 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;

#if USE_X11_GLYPH_SUBSTITUTION
// This string is used to create a font set
// It should contain characters for which the default font likely has no glyphs
static const char*  ui_gs_test_text =
   "\xE6\x97\xA5"       // Japanese symbol
   "\xE3\x84\x86"       // Chinese symbol
   "\xEA\xB0\x94"       // Korean symbol
   "\xE0\xB0\x95"       // Teluga symbol
   "\xE0\xB2\x95"       // Kannada symbol
   "\xE3\x80\x86"       // CJK IDEOGRAPHIC CLOSING MARK
   "\xF0\x9D\x84\x9E";  // Musical G-clef
#endif  // USE_X11_GLYPH_SUBSTITUTION


// =============================================================================
// Forward declarations

#if USE_X11_GLYPH_SUBSTITUTION
extern "C"
{
   extern void  set_xft_substitution_state(const int);
   // Search for fonts that have similar properties to some face already loaded
   extern int  fl_xft_font_set_find_similar(const char*, const Fl_Font, char**,
                                            int*);
}
#endif  // USE_X11_GLYPH_SUBSTITUTION


// =============================================================================
// 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, 14);
}


// =============================================================================
// 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 (http://www.fltk.org/)" << "\n"
      << "- The Unicode\xC2\xAE Standard (http://www.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 (http://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 (http://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 ISO8859-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 ISO8859-1
      //print_error("UI: Non UTF-8 locale, converting string to ISO8859-1");
      len = fl_utf8toa(s, std::strlen(s), NULL, 0);
      tmp = new char[len + 1U];
      if (fl_utf8toa(s, std::strlen(s), tmp, len + 1U) != len)
      {
         delete[] tmp;
      }
      else  { res = (const char*) tmp; }
   }
   else
   {
      // Yes => Return string unchanged
      len = 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


// =============================================================================
// 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("UI: 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);
}


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

static void  gui_cite_content(Fl_Text_Buffer*  compText, const char*  ca)
{
   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("UI: 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);
   if(NULL == intro)  { compText->insert(0, "[Error] wrote:\n"); }
   else
   {
      compText->insert(0, "\n");
      compText->insert(0, intro);
   }
   core_free((void*) intro);
   // Insert empty line after citation
   compText->append("\n");
}


// =============================================================================
// 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;
         }
         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

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

   // Intercept key pressed events
   switch(event)
   {
      case FL_KEYDOWN:
      {
         // Enter key pressed
         if (Fl::event_key() == FL_Enter)
         {
#if 0
            // 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
            res = 1;
            break;
         }
         // Space key pressed
         else if (Fl::event_key() == 0x20)
         {
            mainWindow->ascrolldown_cb(true);
            res = 1;
            break;
         }
         // No 'break' here is intended, use default handler if ignored
      }
      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_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_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_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:
            {
               // Ignore
               break;
            }
            case EVENT_GL_REFRESH_EXIT:
            {
               mainState = STATE_READY;
               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:
            case EVENT_POST_EXIT:
            {
               // Preserve state
               break;
            }
            case EVENT_COMPOSE_EXIT:
            {
               mainState = STATE_READY;
               break;
            }
            default:
            {
               // Reject all other operations
               res = false;
               break;
            }
         }
         break;
      }
      default:
      {
         // This should never be executed, try to recover
         print_error("UI: Error in 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  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 hight for ISO216 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());
         // 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.scrollbar_width(0);
         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;
         lpp = 0;
         while(canvas.position_to_xy(i, &x, &y))
         {
            if(!canvas.move_down())  { break; }
            i = canvas.insert_position();
            ++lpp;
         }
         if(lines > lpp)
         {
            // Remove one line because the last one is maybe only partly visible
            --lpp;  if(0 >= lpp)  { res = -1; }
            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; }
            }
         }
      }
      // Setup printer
      if(!res)
      {
         res = printer.start_job(pages, &frompage, &topage);
         if(!res)
         {
            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; }
                        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();
         }
      }
   }

   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*  homedir = core_get_homedir();

      fl_file_chooser_ok_label(S("Save"));
      if(NULL != homedir)
      {
         pathname = fl_file_chooser(title, "*", homedir, 0);
         if(NULL != pathname)
         {
            rv = currentArticle->savefile(pathname);
            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*) homedir);
      }
   }
}


// =============================================================================
// 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 "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 MiscCfgWindow(title);
      delete[] title;
   }
#else  // CFG_USE_XSI && !CFG_NLS_DISABLE
   new MiscCfgWindow(titleString);
#endif  // CFG_USE_XSI && !CFG_NLS_DISABLE
}


// =============================================================================
// Main Window identity information 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 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] = 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);
      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
   {
      olddata = currentArticle->text();
      if(NULL != olddata)
      {
         enc_rot13(olddata);
         newdata->text(olddata);
         articleUpdate(newdata);
      }
      std::free((void*) olddata);
   }
}


// =============================================================================
// Main Window mark single article as unread callback

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();
      ti->labelfont(FL_HELVETICA_BOLD);
      a = ((core_hierarchy_element*) ti->user_data())->anum;
      core_mark_as_unread(&group_list[group_list_index], a);
      articleTree->redraw();
      groupListUpdateEntry(group_list_index);
      UI_STATUS(S("Marked article as unread."));
   }
}


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

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

   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 as 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 as read."));
   }
}


// =============================================================================
// 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:
      {
         // 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 as read)
               // if there are no unread children or they are open/visible
               ti->labelfont(FL_HELVETICA);
            }
            // 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*  s = NULL;
   char*  t = NULL;
   std::size_t  i = 0;
   char*  sig;
   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("UI: 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 RFC2606
               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
                  // Attention:
                  // Overloaded and both prototypes different than in C!
                  sig = std::strstr(q, "\n-- \n");
                  if(NULL != sig)  { sig[1] = 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);
                  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;

   // 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->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);
               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("UI: Error in main window state machine");
      }
   }
}


// =============================================================================
// 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) 5);
   }
   progress_percent_label[4] = 0;
}


// =============================================================================
// 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;
   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("UI: 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);
                  }
               }
            }
            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)
      {
         // 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
            rv = fl_choice("%s", S("Yes"),
                           S("No"), NULL,
                           S("Store the password from the subsequent request?"));
            if(!rv)  { conf_ephemeral_passwd = 0; }
            else  { conf_ephemeral_passwd = 1; }
            pass = fl_password("%s", config[CONF_PASS].val.s, "Password:");
            if(NULL != pass)  { conf_string_replace(&config[CONF_PASS], pass); }
         }
         // Reset CRL update interval back to epoch (force new CRLs)
         conf_string_replace(&config[CONF_CRL_UPD_TS], "1970-01-01T00:00:00Z");
         // 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("UI: Error in main window state machine");
      }
   }
}


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

void  MainWindow::groupSubscribe(int  action)
{
   int  rv = -1;
   std::size_t  groupcount;
   core_groupdesc*  grouplist;
   std::size_t  i, ii;
   char*  name;

   // 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);
      }
   }
   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 group list failed."));
         UI_READY();
      }
      else
      {
         // Operation finished
         core_mutex_lock();
         groupcount = data.size;
         grouplist = (core_groupdesc*) data.data;
         core_mutex_unlock();
         UI_STATUS(S("Group list updated."));
         UI_READY();
         // 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);
         SC("")
         // Add groups
         for(i = 0; i < groupcount; ++i)
         {
            // Do not remove this check until you know what you are doing!
            if(CORE_GROUP_FLAG_ASCII & grouplist[i].flags)
            {
               name = grouplist[i].name;
               ii = 0;
               do  { if('.' == name[ii])  { name[ii] = '/'; } }
               while(name[ii++]);
               subscribeWindow->add(name);
            }
         }
         // Collapse all branches of tree
         subscribeWindow->collapseAll();
         // The destructor of 'subscribeWindow' will release the memory for
         // 'grouplist'. Because this window is modal, no other operations are
         // possible in the meanwhile.
      }
      if(!stateMachine(EVENT_SUBSCRIBE_EXIT))
      {
         print_error("UI: 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 refresh group list with subscribed groups
// (must be locked)

void  MainWindow::groupListRefresh(int  action)
{
   int  rv = -1;
   std::size_t  i;

   // Start core to do the work
   if(UI_CB_START == action)
   {
      if(!stateMachine(EVENT_GL_REFRESH))  { rv = 1; }
      else
      {
         rv = groupStateMerge();
         if(!rv)
         {
            // Get group states from article source
            UI_STATUS(S("Refreshing subscribed groups ..."));
            UI_BUSY();
            groupRefresh_cb_state = groupList->value();
            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 article source
      if(group_num)
      {
         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
         UI_STATUS(S("Refreshing subscribed groups failed."));
      }
      else
      {
         if(!group_num || unsub)
         {
            // Delete article tree and current article
            clearTree();
            core_free(currentGroup);
            currentGroup = NULL;
         }
         else
         {
            // Reset group list widget
            groupList->value(groupRefresh_cb_state);
         }
         // Operation finished
         UI_STATUS(S("Subscribed groups refreshed."));
      }
      unsub = false;
      UI_READY();
      if(!stateMachine(EVENT_GL_REFRESH_EXIT))
      {
         print_error("UI: 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)  { rv = 1; }
      else if(!stateMachine(EVENT_G_SELECT))
      {
         // Reset group list window if operation is not allowed
         if((std::size_t) INT_MAX > group_list_index)
         {
            groupList->value((int) group_list_index + 1);
         }
         rv = 1;
      }
      else
      {
         // Start core to do the work
         if(0 < index)
         {
            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)
      {
         UI_STATUS(S("Setting new current group failed."));
         if(stateMachine(EVENT_AT_REFRESH))
         {
            if(!stateMachine(EVENT_AT_REFRESH_EXIT))
            {
               print_error("UI: Error in main window state machine");
            }
         }
      }
      else
      {
         // Success, update current group
         core_free(currentGroup);
         core_mutex_lock();
         currentGroup = (core_groupdesc*) data.data;
         core_mutex_unlock();
         // Clamp article count to process
         groupCAC(currentGroup);
         UI_STATUS(S("New current group set."));

         // Update article tree
         currentArticleHE = NULL;
         updateArticleTree(UI_CB_START);
      }
      if(!stateMachine(EVENT_G_SELECT_EXIT))
      {
         print_error("UI: 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 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 there is a new Fl_Tree widget. We don't use it until switching
// ABI to 1.4 (otherwise we break compatibility to shared 1.3.x libraries).

void  MainWindow::scrollTree(ui_scroll  position, Fl_Tree_Item*  ti)
{
   // Move 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); }
   // Scroll after redraw is complete
   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;
      }
   }
}


// =============================================================================
// 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 as unread).
// Then the corresponding article is checked. If it's already read, the font is
// changed to normal style (marked as 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;
   const char*  f;  // From
   std::size_t  f_len;
   char*  f_tmp;  // Temporary buffer used to strip '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;

   // Set default font to bold (mark as 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])  { ss[ssi++] = s[ii++]; }
      // -----------------------------------------------------------------------
      // 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 as 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);
      }

      // 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;

   // 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(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();
}


// =============================================================================
// 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;
         // Init article hierarchy
         rv = core_hierarchy_manager(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 as 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(CORE_HIERARCHY_ADD, ai - 1U, header);
               if(0 > rv)
               {
                  print_error("UI: 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->value(groupSelect_cb_state);
         // Unlock before updating article tree widget
         if(!stateMachine(EVENT_AT_REFRESH_EXIT))
         {
            print_error("UI: 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("UI: 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 RFC3986 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
   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  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;

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

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

   // 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;
         hyperlink = false;
         citation = false;
         continue;
      }
      if(hyperlink)
      {
         // Check for end of hyperlink
         // According to RFC3986 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(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; }  else  { 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])
            {
               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);
   }
}


// =============================================================================
// Main window format and 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) 13
#define UI_HDR_PAD(s, n)  { for(i = 0; i < n; ++i)  { s << " "; } }
const char*  MainWindow::printHeaderFields(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")
   };
   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";
   }
   hdrData << std::flush;

   // Attention:
   //
   //    const char*  s = hdrData.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&  hs = hdrData.str();

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

   return(s);
}


// =============================================================================
// 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;
   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*  body;
   char  last_char;
   bool  overview = false;
   bool  process = false;

   // Check whether current HE is available
   if(NULL == currentArticleHE)
   {
      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(ui_debug)  { print_error("UI: Article selected"); }
         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(CORE_HIERARCHY_UPDATE,
                                           currentArticleHE->anum, p);
               if(0 > rv)  { process = false; }
            }
         }
      }
      if(!process)
      {
         // Failed
         UI_STATUS(S("Download of article failed."));
         print_error("UI: 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;
         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)
            {
               // Print delimiter between entities
               if(i)
               {
                  // Append a linefeed if last entity don't have one at its end
                  last_char = tb->byte_at(tb->length() - 1);
                  if((char) 0x0A != last_char)  { tb->append("\n"); }
                  tb->append(ENC_DELIMITER);
               }
               body = mimeData->part(i, &cte, &ct);
               // RFC2049 mandates that unknown subtypes of "text" must be
               // viewable raw/undecoded => We handle them as subtype "plain"
               // (print them undecoded)
               if(ENC_CT_TEXT != ct->type)
               {
                  // Error: Content is not supported
                  switch(ct->type)
                  {
                     case ENC_CT_IMAGE:
                     {
                        tb->append("[image]");
                        break;
                     }
                     case ENC_CT_MULTIPART:
                     {
                        tb->append("[nested multipart]");
                        break;
                     }
                     case ENC_CT_APPLICATION:
                     {
                        tb->append("[application]");
                        break;
                     }
                     default:
                     {
                        tb->append("[unknown]");
                        break;
                     }
                  }
                  tb->append("\n");
                  tb->append(S("MIME content type not supported"));
               }
               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(ct->flags
                                                   & ENC_CT_FLAG_DELSP, p);
                        if(NULL == q)
                        {
                           print_error("UI: 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
                     // (RFC822) form to POSIX form
                     q = core_convert_canonical_to_posix(p);
                     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); }
                     }
                  }
               }
            }
         }
         // Print content
         articleUpdate(tb);
         // Store last viewed article
         group_list[group_list_index].last_viewed = currentArticleHE->anum;
      }
      // Mark current article as read
      core_mark_as_read(&group_list[group_list_index],
                        currentArticleHE->anum);
      groupListUpdateEntry(group_list_index);
      // Check for error
      if(!process)  { currentArticleHE = NULL; }
      if(!stateMachine(EVENT_A_SELECT_EXIT))
      {
         print_error("UI: 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("Downloading message of the day 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 = enc_convert_canonical_to_posix("[Invalid encoding]\r\n");
         }
         else  { motd = enc_convert_canonical_to_posix(p); }

         // 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("UI: Error in main window state machine");
      }
   }
}


// =============================================================================
// Main window view article source 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 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("UI: 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*  domain = config[CONF_DOMAIN].val.s;
   std::ostringstream  titleString;
   char*  p = NULL;
   char*  q = NULL;
   const char*  cq = NULL;
   std::size_t  i = 0;
   char*  sig;
   struct core_article_header*  hdr = NULL;
   char*  from = NULL;
   std::size_t  len;
   Fl_Text_Buffer  header;
   const char*  msgid = 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(!groupList->value())
   {
      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("OK"),
                           S("Cancel"),
                           S("Ignore"),
                           S("Really execute Followup-To to poster?"));
            if(!rv)  { sendEmail(); }
            if(!rv || 1 == rv)  { abort = true; }
         }
      }
   }
   if(!abort)
   {
      // Prepare current article content for citation
      if(reply)
      {
         p = currentArticle->text();
         if(NULL != p)
         {
            // Strip header
            while('_' != p[i++]);
            while('|' != p[i++]);
            while('|' != p[i++]);
            q = &p[++i];
            // Strip signature
            // Attention: Overloaded and both prototypes different than in C!
            sig = std::strstr(q, "\n-- \n");
            if(NULL != sig)  { sig[1] = 0; }
            cq = q;
            // Extract author name
            len = std::strlen(hdr->from);
            from = new char[++len];
            std::strcpy(from, hdr->from);
            stripAngleAddress(from);
         }
      }
      else if(super)  { cq = "This is a cancel control message\n"; }

      // Create message header
      //
      // According to RFC5536 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 RFC5537 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(domain))
      {
         header.append("Message-ID: ");
         msgid = core_get_msgid(domain);
         if(NULL == msgid)  { headerOK = false; }
         else  { header.append(msgid); }
         // Memory is released later because of Cancel-Lock!
         header.append("\n");
         insertedMessageID = true;
      }
      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(reply)
      {
         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; }
      }
      else  { header.append(currentGroup->name); }
      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)  { header.append(hdr->subject); }
      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
         ckey = core_get_cancel_lock(msgid);
         if(NULL != ckey)
         {
            header.append("Cancel-Lock: ");
            header.append(ckey);
            header.append("\n");
         }
         core_free((void*) ckey);
         // RFC5536 requires that this header is inserted at injection.
         // According to RFC5537 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
               ckey = core_get_cancel_key(msgid);
               if(NULL != ckey)
               {
                  header.append("Cancel-Key: ");
                  header.append(ckey);
                  header.append("\n");
               }
               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 RFC5537 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");
      }
      header.append("User-Agent: " CFG_NAME "/" CFG_VERSION
                    " (for " CFG_OS ")\n");
      // Insert MIME header field
      // Note: It is mandatory for a MIME compliant 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 ISO8859-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("UI: 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
         // (RFC822) 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("UI: 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 hyperlink handling callback

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

   //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("UI: 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 RFC1738 this specify a newsgroup or a Message-ID
            // An asterisk is allowed as wildcard => This is not supported
            // According to RFC5538 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))
            {
               // Yes => Check whether URI points to newsgroup or Message-ID
               // Attention: Overloaded and both prototypes different than in C!
               if(!std::strchr(&uri[len], (int) '@'))
               {
                  // Select newsgroup
                  rv = enc_percent_decode(&uri[len]);
                  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 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"));
                  }
                  else 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
                  {
                     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->select((int) gi);
                                    groupSelect(UI_CB_START, (int) gi);
                                    abort = false;
                                 }
                                 break;
                              }
                           }
                        }
                     }
                     if(abort)
                     {
                        rv = fl_choice("%s", S("Yes"),
                                       S("No"), 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]);
                  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
                  {
                     ti = articleTree->root();
                     // Assignment in truth expression is intended
                     while(NULL != (ti = articleTree->next(ti)))
                     {
                        mid = ((core_hierarchy_element*) ti->user_data())
                              ->header->msgid;
                        clen = std::strlen(&uri[len]);
                        // Note: The Message-ID from header has angle brackets
                        if(std::strlen(mid) != clen + (std::size_t) 2)
                        {
                           continue;
                        }
                        if(!std::strncmp(&mid[1], &uri[len], clen))
                        {
                           // 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;
                        }
                     }
                     if(NULL == ti)
                     {
                        SC("Do not use non-ASCII for the translation of this item")
                        fl_message_title(S("Error"));
                        fl_alert("%s", S("Message-ID not found"));
                     }
                  }
               }
            }
            else
            {
               // Check for "mailto:" URI
               len = std::strlen(mailto);
               if(std::strlen(uri) > len && !std::strncmp(uri, mailto, len))
               {
                  // 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
               {
                  // Use external URI handler for all other clickable links
                  rv = ext_handler_uri(uri);
               }
               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");
   const Fl_Color  styles_colors[UI_STYLES_LEN] =
   {
      FL_BLACK,                        // Plain bold
      FL_GRAY_RAMP + (Fl_Color) 10,    // Signature
      FL_RED,                          // External citation
      FL_BLUE,                         // Hyperlink (never change this color!)
      // -----------------------------------------------------------------------
      // Keep the following group continuous
      FL_BLACK,                        // Content
      FL_COLOR_CUBE + (Fl_Color) 84,   // Thread citation level 1 (">  ")
      FL_COLOR_CUBE + (Fl_Color) 96,   // Thread citation level 2 ("> >  ")
      FL_COLOR_CUBE + (Fl_Color) 35,   // Thread citation level 3 ("> > >  ")
      FL_COLOR_CUBE + (Fl_Color) 19    // Thread citation level 4 ("> > > >  ")
   };
   Fl_Pack*  mainGroup;
   Fl_Pack*  statusGroup;
   Fl_Tree_Item*  ti;
   int  i;

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

   // Init group list, current group and current article
   group_num = 0;
   group_list = NULL;
   group_list_index = 0;
   subscribedGroups = NULL;
   currentGroup = NULL;
   currentArticle = NULL;
   currentStyle = NULL;
   currentArticleHE = NULL;
   currentLine = 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_1_1
               << "1.0" << "\n"
#     else  // !CFG_USE_OPENSSL_API_1_1
               << "1.1" << "\n"
#     endif  // !CFG_USE_OPENSSL_API_1_1
#  endif  // CFG_USE_LIBRESSL
#else  // CFG_USE_TLS
               << S("Not available") << "\n"
#endif  // CFG_USE_TLS
               << S("Required FLTK ABI") << ": " << FL_MAJOR_VERSION << "."
               << FL_MINOR_VERSION  << "\n"
               << 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_Pack(0, 0, 730, 395);
   mainGroup->type(Fl_Pack::VERTICAL);
   mainGroup->begin();
   {
      // Menu bar
#if USE_COCOA_SYS_MENUBAR
      menu = new Fl_Sys_Menu_Bar(0, 0, 730, 0);
#else  // USE_COCOA_SYS_MENUBAR
      menu = new Fl_Menu_Bar(0, 0, 730, 30);
#endif  // USE_COCOA_SYS_MENUBAR
      menu->selection_color(UI_COLOR_MENU_SELECTION);
      menu->box(FL_FLAT_BOX);
      // --------------------
      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("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  "), "^x", 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);
      // --------------------
      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);
      // --------------------
      i = FL_MENU_TOGGLE;
      if(config[CONF_TVIEW].val.i)  { i |= FL_MENU_VALUE; }
      menu->add(S("&Article/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("&Article/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("&Article/Only unread articles  "), 0, onlyur_cb,
                (void*) this, i);
      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/View source  "), "^v", viewsrc_cb, (void*) this);
      menu->add(S("&Article/Mark as unread  "), "^u", msau_cb, (void*) this);
      menu->add(S("&Article/Mark all in group as read  "), "^a", maar_cb,
                (void*) this);
      // --------------------
      menu->add(S("&Tools/Debug mode  "), 0, debug_cb, (void*) this,
                FL_MENU_TOGGLE);
      menu->add(S("&Tools/Protocol console  "), 0, console_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 USE_COCOA_SYS_MENUBAR
      contentGroup = new Fl_Tile(0, 0, 730, 370);
#else  // USE_COCOA_SYS_MENUBAR
      contentGroup = new Fl_Tile(0, 30, 730, 340);
#endif  // USE_COCOA_SYS_MENUBAR
      contentGroup->begin();
      {
         // Group list
#if USE_COCOA_SYS_MENUBAR
         groupList = new Fl_Hold_Browser(0, 0, 230, 370);
#else  // USE_COCOA_SYS_MENUBAR
         groupList = new Fl_Hold_Browser(0, 30, 230, 340);
#endif  // USE_COCOA_SYS_MENUBAR
         groupList->callback(gselect_cb, (void*) this);
         groupList->textsize(USE_CUSTOM_FONTSIZE);
#if USE_COCOA_SYS_MENUBAR
         contentGroup2 = new Fl_Tile(230, 0, 500, 370);
#else  // USE_COCOA_SYS_MENUBAR
         contentGroup2 = new Fl_Tile(230, 30, 500, 340);
#endif  // USE_COCOA_SYS_MENUBAR
         contentGroup2->begin();
         {
            // Article tree
#if USE_COCOA_SYS_MENUBAR
            articleTree = new My_Tree(230, 0, 500, 140);
#else  // USE_COCOA_SYS_MENUBAR
            articleTree = new My_Tree(230, 30, 500, 140);
#endif  // USE_COCOA_SYS_MENUBAR
            articleTree->showroot(0);
            articleTree->item_labelsize(USE_CUSTOM_FONTSIZE);
            ti = articleTree->add(S("No articles"));
            if(NULL != ti)  { ti->deactivate(); }
            articleTree->callback(aselect_cb, (void*) this);
            // Article content window
#if USE_COCOA_SYS_MENUBAR
            text = new My_Text_Display(230, 140, 500, 230);
#else  // USE_COCOA_SYS_MENUBAR
            text = new My_Text_Display(230, 170, 500, 200);
#endif  // USE_COCOA_SYS_MENUBAR
            text->callback(hyperlink_cb, (void*) this);
            text->textfont(FL_COURIER);
            text->textsize(USE_CUSTOM_FONTSIZE);
         }
         contentGroup2->end();
      }
      contentGroup->end();

      // Status group
      statusGroup = new Fl_Pack(0, 370, 730, 25);
      statusGroup->type(Fl_Pack::HORIZONTAL);
      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_BLACK;
   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 specify 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; }
   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 RFC4643"));
         scfgAuthUser->selection_color(UI_COLOR_RADIO_BUTTON);
      }
      grpAuth->end();
      grpAuth->align(FL_ALIGN_TOP_LEFT);
      grpAuth->box(FL_EMBOSSED_BOX);
      if(UI_ENC_NONE == sc->enc)
      {
         scfgAuthOff->set();
         grpAuth->deactivate();
      }
      else
      {
         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);
   show();
}


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

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


// =============================================================================
// Identity configuration window constructor
//
// 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);
   show();
}


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

IdentityCfgWindow::~IdentityCfgWindow(void)
{
}


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

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(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, 200, 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 identity window
   copy_label(label);
   set_modal();
   callback(cancel_cb, (void*) this);

   // Create widget group
   gp1 = new Fl_Group(0, 0, 400, 200);
   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 = 200 - gh;

      cfgTabs = new Fl_Tabs(0, 0, 400, gy);
      cfgTabs->begin();
      {
         cacGroup = new Fl_Group(0, th - tg, 400, 200 - 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
            // Resizable space below input fields
            bp = new Fl_Box(0, 150, 400, gy - 150);
            //bp->box(FL_DOWN_BOX);
         }
         cacGroup->end();
         cacGroup->resizable(bp);

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

         qsGroup = new Fl_Group(0, th - tg, 400, 200 - 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, 150 + gh, 0, 0);
   show();
}


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

MiscCfgWindow::~MiscCfgWindow(void)
{
}


// =============================================================================
// 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, std::size_t*  count)
{
   MIMEContentListElement*  res = NULL;  // Result list
   MIMEContentListElement*  cle = NULL;  // Current list element(s)
   MIMEContentListElement*  ble = NULL;  // Buffered list element(s)
   MIMEContentListElement*  lle = NULL;  // Last list element
   std::size_t  ae;  // Additional elements
   std::size_t  bae = 0;  // Buufered additional elements
   std::size_t  num;
   enc_mime_mpe*  mpe = NULL;  // Multipart entities
   enc_mime_cte  cte = ENC_CTE_BIN;
   enc_mime_ct  ct;
   bool  nested_multi;
   char  nested_boundary[ENC_BO_BUFLEN];
   bool  nested_alt;
   core_article_header*  e_h;
   std::size_t  e_len;
   const char*  q;
   std::size_t  i;

   // Split multipart message into entities
   num = enc_mime_multipart(p, &ct, boundary, &mpe);
   for(i = 0; i < num; ++i)
   {
      nested_multi = false;
      nested_alt = 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("UI: Invalid entity in MIME multipart message");
         e_len = mpe[i].len;
         cte = ENC_CTE_BIN;
         ct.type = ENC_CT_APPLICATION;
         ct.subtype = ENC_CTS_OCTETSTREAM;
         ct.charset = ENC_CS_UNKNOWN;
      }
      else
      {
         p = q;
         // Default to "text/plain; charset=US-ASCII" as defined by RFC2046
         ct.type = ENC_CT_TEXT;
         ct.subtype = ENC_CTS_PLAIN;
         ct.charset = ENC_CS_ASCII;
         // Check whether content type header field exist
         if(NULL != e_h->mime_ct)
         {
            // Yes => Check whether content transfer encoding is supported
            cte = enc_mime_get_cte(e_h->mime_cte);
            if(ENC_CTE_UNKNOWN == cte)
            {
               // No => Treat as arbitrary binary data
               cte = ENC_CTE_BIN;
               // RFC2049 mandates 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:
                  {
                     // Check character set
                     if(ENC_CS_UNKNOWN == ct.charset)
                     {
                        // Content character set not supported
                        // RFC2049 mandates 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:
                  {
                     break;
                  }
                  case ENC_CT_MULTIPART:
                  {
                     nested_multi = true;
                     if(ENC_CTS_ALTERNATIVE == ct.subtype)
                     {
                        nested_alt = 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;
                  }
               }
            }
         }
         core_destroy_entity_header(&e_h);
      }
      // 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
               continue;
            }
         }
      }
      // Create new content element(s)
      if(nested_multi)
      {
         // Recursively decode nested multipart entities
         cle = decodeElement(p, e_len, nested_boundary, nested_alt, &ae);
      }
      else
      {
         // Create new element
         cle = initElement(p, e_len, cte, &ct);
         ++ae;
      }
      // 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;
      }
   }
   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;
   }

   return(res);
}


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

MIMEContent::MIMEContentListElement*
MIMEContent::initElement(const char*  p, std::size_t  len,
                         enc_mime_cte  cte, enc_mime_ct*  ct)
{
   MIMEContentListElement*  cle = NULL;
   char*  cp;

   cle = new MIMEContentListElement;
   cp = new char[len + (std::size_t) 1];
   std::strncpy(cp, p, len);  cp[len] = 0;
   cle->content = cp;
   cle->cte = cte;
   std::memcpy((void*) &cle->ct, (void*) ct, sizeof(enc_mime_ct));
   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

   // 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("UI: Trying to decode MIME article with invalid header");
      }
   }
   // Default to "text/plain; charset=US-ASCII" as defined by RFC2045
   multi = false;
   alt = 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;
         // RFC2049 mandates 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
                  // RFC2049 mandates 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:
            {
               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; }
               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)
   {
      partList = initElement(p, std::strlen(p), cte, &ct);
      partNum = 1;
   }
   else
   {
      // Recursively create elements from multipart entities
      partList = decodeElement(p, std::strlen(p), boundary, alt, &partNum);
   }
}


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

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

   while(NULL != cle)
   {
      nle = cle->next;
      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  name[1024];

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

   // Update subscribed groups
   mainWindow->groupStateExport();
   while(NULL != i)
   {
      rv = subscribeTree->item_pathname(name, 1024, i);
      if(rv)
      {
         if(-2 == rv)  { print_error("UI: Group name too long and ignored"); }
         else  { print_error("UI: 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("UI: 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) :
   UI_WINDOW_CLASS(730, 350, label), grouplist(glist)
{
   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);
      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);
   show();
}


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

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


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

void  ProtocolConsole::update(void)
{
   int  rv;
   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(logname);  logname = NULL;
            if(!logfp)
            {
               nolog = 1;
               protocolConsole->consoleDisplay->insert(S("No logfile"));
            }
         }
      }
      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->textsize(USE_CUSTOM_FONTSIZE);
   consoleDisplay->buffer(consoleText);
   resizable(consoleDisplay);
   callback(exit_cb, (void*) this);
   show();
}


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

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

   // Release memory
   delete consoleDisplay;
   delete consoleText;

   protocolConsole = NULL;
}


// =============================================================================
// 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);
   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->textsize(USE_CUSTOM_FONTSIZE);
   motdDisplay->buffer(motdText);
   resizable(motdDisplay);
   callback(exit_cb, (void*) this);
   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->textsize(USE_CUSTOM_FONTSIZE);
   licenseDisplay->buffer(licenseText);
   resizable(licenseDisplay);
   callback(exit_cb, (void*) this);
   show();
}


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

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


// =============================================================================
// 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");
   const char*  homedir = core_get_homedir();

   fl_file_chooser_ok_label(S("Save"));
   if(NULL != homedir)
   {
      pathname = fl_file_chooser(title, "*", homedir, 0);
      if(NULL != pathname)
      {
         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*) homedir);
   }
}


// =============================================================================
// 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 (RFC822) form
   // to POSIX form
   srcText = new Fl_Text_Buffer();
   srcStyle = new Fl_Text_Buffer();
   ap = core_convert_canonical_to_posix(article);
   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);
   }

   // Configure article source 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->textsize(USE_CUSTOM_FONTSIZE);
      srcDisplay->buffer(srcText);
      resizable(srcDisplay);
      // Create content and style buffers
      styles = new Fl_Text_Display::Style_Table_Entry[3];
      styles[0].color = FL_BLACK;
      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);
   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*  sig;
   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);
      // Attention: Overloaded and both prototypes different than in C!
      sig = std::strstr(body, "\n-- \n");
      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(Fl_Widget*  w)
{
   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,
                                       int  nRestyled, const char*  deletedText,
                                       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("UI: 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 = (std::size_t) 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(Fl_Widget*  w)
{
   const char*  domain = config[CONF_DOMAIN].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*  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 RFC5536
      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; }
               // RFC5536 forbid to use of "all" and recommend 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)
                  {
                     p[i] = (char) std::tolower((int) q[i]);
                  }
                  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 RFC5536
                  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(domain))
         {
            msgid = core_get_msgid(domain);
            if(NULL != msgid)
            {
               replaceHeaderField("Message-ID", msgid);
               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("OK"),
                        S("Cancel"), 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("OK"),
                              S("Cancel"), 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(Fl_Widget*  w)
{
   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_BLACK;
         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)  { gui_cite_content(compText, ca); }
         }
         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]);
               }
            }
         }
         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: UI: "
                        "Setting keywords from config file for test group\n",
                        CFG_NAME);
            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 ISO8601 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
            std::printf("%s: UI: Following former distribution\n", CFG_NAME);
            distriField->value(hdr->dist);
         }
         else
         {
            // Follow suggestion of article source
            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: UI: Setting distribution as suggested\n",
                           CFG_NAME);
               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))  { 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 ISO8859-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("UI: 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();

      // Show message/choice windows with mouse on default button
      fl_message_hotspot(1);

#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!
      mainWindow->show(argc, argv);

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

      // Set default font
      gui_set_default_font();

#if USE_X11_GLYPH_SUBSTITUTION
      // Enable and configure glyph substitution (EXPERIMENTAL)
      std::printf("%s: UI: Enabling experimental glyph substitution code\n",
                  CFG_NAME);
      Fl_Font  font = FL_FREE_FONT;
      int  missing;
      int  i;
      int  max_fonts = 8;
      char*  font_name[8];

      // Enable glyph substitution in FLTK
      set_xft_substitution_state(1);
      // Configure non-proportional fonts for glyph substitution
      std::printf("%s: UI: Create font set for monospace ...\n", CFG_NAME);
      missing = fl_xft_font_set_find_similar(ui_gs_test_text, FL_COURIER,
                                             font_name, &max_fonts);
      for(i = 0; i < max_fonts; ++i)
      {
         Fl::set_font(font++, font_name[i]);
      }
      // Configure proportional fonts for glyph substitution
      std::printf("%s: UI: Create font set for proportional ...\n", CFG_NAME);
      missing = fl_xft_font_set_find_similar(ui_gs_test_text, FL_HELVETICA,
                                             font_name, &max_fonts);
      for(i = 0; i < max_fonts; ++i)
      {
         Fl::set_font(font++, font_name[i]);
      }
      std::printf("%s: UI: Create font set for proportional bold ...\n",
                  CFG_NAME);
      missing = fl_xft_font_set_find_similar(ui_gs_test_text, FL_HELVETICA_BOLD,
                                             font_name, &max_fonts);
      for(i = 0; i < max_fonts; ++i)
      {
         Fl::set_font(font++, font_name[i]);
      }
      std::printf("%s: UI: %d fonts added\n",
                  CFG_NAME, (int) (font - FL_FREE_FONT));
      std::printf("%s: UI: %d glyphs missing\n", CFG_NAME, missing);
#endif  // USE_X11_GLYPH_SUBSTITUTION

      // 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 && !CFG_TLS_WARNING_DISABLE
      rv = tls_vulnerability_check();
      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 && !CFG_TLS_WARNING_DISABLE

      // 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: UI: TLS certificate CRL updates suppressed\n",
                        CFG_NAME);
            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)
      {
         pass = fl_password("%s", config[CONF_PASS].val.s, "Password:");
         if(NULL != pass)  { conf_string_replace(&config[CONF_PASS], pass); }
      }

      // Initialize group tree
      mainWindow->groupListRefresh(UI_CB_START);

      //! \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 = mainWindow->composeWindow
                    ->compEditor->buffer()->savefile(tmpfile);
               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("UI: 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
      mainWindow->groupStateExport();
   }

   // Release MT lock of FLTK
   Fl::unlock();

   // Shutdown core (and join with the core thread)
   core_exit();

   // 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 use UTF-8 encoding
//! - 0 Locale use other encoding

int  ui_get_locale_utf8(void)
{
   // Up to FLTK 1.3.3, this function return the required information
   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_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_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("UI: Can't assign cookie to callback function");
         rv = -1;
         break;
      }
   }
   if(rv)
   {
      print_error("UI: 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>
//! http://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
