/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/*
 * Pan - A Newsreader for Gtk+
 * Copyright (C) 2002-2006  Charles Kerr <charles@rebelbase.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "body-pane.h"

#include <config.h>
#include <cctype>
#include <cmath>
#include <iostream>
#include <sstream>
#include <glib/gi18n.h>
#include <gtk/gtk.h>
#include <gdk/gdk.h>
#include <gmime/gmime.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <pan/general/debug.h>
#include <pan/general/log.h>
#include <pan/general/macros.h>
#include <pan/general/utf8-utils.h>
#include <pan/general/e-util.h>
#include "pan/gui/load-icon.h"
#include <pan/usenet-utils/gnksa.h>
#include <pan/usenet-utils/mime-utils.h>
#include <pan/usenet-utils/url-find.h>
#include "pad.h"
#include "tango-colors.h"
#include "xface.h"
#include "url.h"
#include "save-attach-ui.h"

#ifdef HAVE_WEBKIT
  #include <webkit/webkit.h>
#endif


#define FIRST_PICTURE "first-picture"

#define FACE_SIZE 48

namespace pan {

/***
****
***/

namespace
{

  GtkWindow* get_window (GtkWidget* w)
  {
    return GTK_WINDOW (gtk_widget_get_toplevel (w));
  }

  enum Icons
  {
    ICON_SIG_OK,
    ICON_SIG_FAIL,
    NUM_ICONS
  };

  struct Icon {
      char const *pixbuf_file;
      GdkPixbuf *pixbuf;
  } icons[NUM_ICONS] = {
    { "icon_sig_ok.png",          nullptr },
    { "icon_sig_fail.png",        nullptr }
  };
}

/***
****
***/

/**
***  Pixbuf Cache
**/
namespace
{
  class PixbufCache
  {
    private:
      typedef std::pair<GdkPixbuf*,GdkPixbuf*> pixbuf_pair_t; // original, scaled
      typedef std::map<int,pixbuf_pair_t> pixbufs_t;
      pixbufs_t _pixbufs;

    public:
      PixbufCache() {}
      ~PixbufCache() { clear(); }

    public:
      void clear () {
        foreach (pixbufs_t, _pixbufs, it) {
          g_object_unref (it->second.first);
          g_object_unref (it->second.second);
        }
        _pixbufs.clear ();
      }

    public:
      bool get_pixbuf_at_offset (int offset, GdkPixbuf*& original, GdkPixbuf*& scaled) {
        if (!_pixbufs.count(offset))
          return false;
        pixbuf_pair_t &p(_pixbufs[offset]);
        original = p.first;
        scaled = p.second;
        return true;
      }
      void set_pixbuf_at_offset (int offset, GdkPixbuf *original, GdkPixbuf *scaled) {
        bool const had_old(_pixbufs.count(offset));
        pixbuf_pair_t &p(_pixbufs[offset]), old(p);
        g_object_ref (p.first = original);
        g_object_ref (p.second = scaled);
        if (had_old) {
          g_object_unref (old.first);
          g_object_unref (old.second);
        }
      }
  };

  void delete_pixbuf_cache (gpointer pc) {
    delete static_cast<PixbufCache*>(pc);
  }

  PixbufCache& get_pixbuf_cache (gpointer gp) {
    static char const *PIXBUF_CACHE("pixbuf-cache");
    GObject * o (G_OBJECT (gp));
    PixbufCache *pc ((PixbufCache*) g_object_get_data(o, PIXBUF_CACHE));
    if (!pc) {
      pc = new PixbufCache ();
      g_object_set_data_full (o, PIXBUF_CACHE, pc, delete_pixbuf_cache);
    }
    return *pc;
  }

  bool get_pixbuf_at_offset (gpointer o, int offset, GdkPixbuf*& original, GdkPixbuf*& scaled) {
    return get_pixbuf_cache(o).get_pixbuf_at_offset (offset, original, scaled);
  }

  void set_pixbuf_at_offset (gpointer o, int offset, GdkPixbuf *original, GdkPixbuf *scaled) {
    get_pixbuf_cache(o).set_pixbuf_at_offset (offset, original, scaled);
  }

  void clear_pixbuf_cache (gpointer o) {
    get_pixbuf_cache(o).clear ();
  }
}

/**
***  "fullsize" flag
**/
namespace
{
  #define FULLSIZE "fullsize"

  void set_fullsize_flag (gpointer o, bool b) {
    g_object_set_data (G_OBJECT(o), FULLSIZE, GINT_TO_POINTER(b));
  }
  bool get_fullsize_flag (gpointer o) {
    return g_object_get_data (G_OBJECT(o), FULLSIZE) != nullptr;
  }
  bool toggle_fullsize_flag (gpointer o) {
    bool const b(! get_fullsize_flag(o));
    set_fullsize_flag (o, b);
    return b;
  }
}

/**
***  Cursors
**/
namespace
{
  enum {
    CURSOR_IBEAM,
    CURSOR_HREF,
    CURSOR_ZOOM_IN,
    CURSOR_ZOOM_OUT,
    CURSOR_QTY
  };

  GdkCursor ** cursors(nullptr);
  GdkCursor * cursor_current(nullptr);

  void free_cursors (void) {
    delete[] cursors;
  }

  void create_cursors() {
    cursors = new GdkCursor*[CURSOR_QTY];
  }

  void init_cursors (GtkWidget * w)
  {
      GdkDisplay * display (gtk_widget_get_display (w));

      int width, height;
      const GtkIconSize size (GTK_ICON_SIZE_LARGE_TOOLBAR);
      GdkPixbuf * pixbuf = gtk_widget_render_icon_pixbuf (w, GTK_STOCK_ZOOM_IN, size);
      g_object_get (G_OBJECT(pixbuf), "width", &width, "height", &height, nullptr);
      cursors[CURSOR_ZOOM_IN] = gdk_cursor_new_from_pixbuf (display, pixbuf, width/2, height/2);
      g_object_unref (G_OBJECT(pixbuf));

      pixbuf = gtk_widget_render_icon_pixbuf (w, GTK_STOCK_ZOOM_OUT, size);
      g_object_get (G_OBJECT(pixbuf), "width", &width, "height", &height, nullptr);
      cursors[CURSOR_ZOOM_OUT] = gdk_cursor_new_from_pixbuf (display, pixbuf, width/2, height/2);
      g_object_unref (G_OBJECT(pixbuf));

      cursors[CURSOR_IBEAM] = gdk_cursor_new (GDK_XTERM);
      cursors[CURSOR_HREF] = gdk_cursor_new (GDK_HAND2);
  }

  void set_cursor (GdkWindow *window, GtkWidget *w, int mode)
  {
    init_cursors(w);
    GdkCursor * cursor_new = cursors[mode];
    if (cursor_new != cursor_current) {
       gdk_window_set_cursor (window, cursor_current=cursor_new);
       g_object_unref(cursor_current);
    }
  }

  void set_cursor_from_iter (GdkWindow *window, GtkWidget *w, GtkTextIter *it)
  {
    GtkTextView * text_view (GTK_TEXT_VIEW (w));
    GtkTextBuffer * buf (gtk_text_view_get_buffer (text_view));
    GtkTextTagTable * tags (gtk_text_buffer_get_tag_table (buf));
    GtkTextTag * pix_tag (gtk_text_tag_table_lookup (tags, "pixbuf"));
    GtkTextTag * url_tag (gtk_text_tag_table_lookup (tags, "url"));
    bool const in_url(gtk_text_iter_has_tag(it, url_tag));
    bool const in_pix(gtk_text_iter_has_tag(it, pix_tag));
    bool const fullsize(get_fullsize_flag(buf));

    int mode;
    if (in_pix && fullsize) mode = CURSOR_ZOOM_OUT;
    else if (in_pix)        mode = CURSOR_ZOOM_IN;
    else if (in_url)        mode = CURSOR_HREF;
    else                    mode = CURSOR_IBEAM;
    set_cursor (window, w, mode);
  }
}


namespace
{
GtkTextTag *get_named_tag_from_view(GtkWidget *w, char const *key)
{
    GtkTextView * text_view (GTK_TEXT_VIEW(w));
    GtkTextBuffer * buf = gtk_text_view_get_buffer (text_view);
    GtkTextTagTable * tags = gtk_text_buffer_get_tag_table (buf);
    return gtk_text_tag_table_lookup (tags, key);
  }

  void get_iter_from_event_coords (GtkWidget * w,
                                   int x, int y,
                                   GtkTextIter * setme)
  {
    GtkTextView * text_view (GTK_TEXT_VIEW (w));
    gtk_text_view_window_to_buffer_coords (text_view,
                                           GTK_TEXT_WINDOW_WIDGET,
                                           x, y, &x, &y);
    gtk_text_view_get_iter_at_location (text_view, setme, x, y);
  }

  std::string get_url_from_iter (GtkWidget * w, GtkTextIter * iter)
  {
    std::string ret;
    GtkTextTag * url_tag (get_named_tag_from_view (w, "url"));
    if (gtk_text_iter_has_tag (iter, url_tag))
    {
      GtkTextIter begin(*iter), end(*iter);
      if (!gtk_text_iter_begins_tag (&begin, url_tag))
        gtk_text_iter_backward_to_tag_toggle (&begin, nullptr);
      gtk_text_iter_forward_to_tag_toggle (&end, nullptr);
      char * pch = gtk_text_iter_get_text (&begin, &end);
      if (pch) {
        ret = pch;
        g_free (pch);
      }
    }
    return ret;
  }

  gboolean motion_notify_event (GtkWidget       * w,
                                GdkEventMotion  * event,
                                gpointer          hover_url)
  {
    if (event->window != nullptr)
    {
      int x, y;
      if (event->is_hint)
        gdk_window_get_device_position (event->window, event->device, &x, &y, nullptr);
      else {
        x = (int) event->x;
        y = (int) event->y;
      }
      GtkTextIter iter;
      get_iter_from_event_coords (w, x, y, &iter);
      set_cursor_from_iter (event->window, w, &iter);
      *static_cast<std::string*>(hover_url) = get_url_from_iter (w, &iter);
    }

    return false;
  }

  /* returns a GdkPixbuf of the scaled image.
     unref it when no longer needed. */
  GdkPixbuf *size_to_fit(GdkPixbuf *pixbuf, GtkAllocation const *size)
  {
    int const nw(size ? size->width : 0);
    int const nh(size ? size->height : 0);

    GdkPixbuf * out (nullptr);
    if (nw>=100 && nh>=100)
    {
      int const ow(gdk_pixbuf_get_width(pixbuf));
      int const oh(gdk_pixbuf_get_height(pixbuf));
      double scale_factor (std::min (nw/(double)ow, nh/(double)oh));
      scale_factor = std::min (scale_factor, 1.0);
      int const scaled_width((int)std::floor(ow * scale_factor + 0.5));
      int const scaled_height((int)std::floor(oh * scale_factor + 0.5));
      out = gdk_pixbuf_scale_simple (pixbuf,
                                     scaled_width, scaled_height,
                                     GDK_INTERP_BILINEAR);
    }

    if (!out)
    {
      g_object_ref (pixbuf);
      out = pixbuf;
    }

    return out;
  }

  void resize_picture_at_iter(GtkTextBuffer *buf,
                              GtkTextIter *iter,
                              bool fullsize,
                              GtkAllocation const *size,
                              GtkTextTag *apply_tag)
  {

    int const begin_offset(gtk_text_iter_get_offset(iter));

    GdkPixbuf * original (nullptr);
    GdkPixbuf * old_scaled (nullptr);
    if (!get_pixbuf_at_offset (buf, begin_offset, original, old_scaled))
      return;

    GdkPixbuf * new_scaled (size_to_fit (original, (fullsize ? nullptr : size)));
    int const old_w(gdk_pixbuf_get_width(old_scaled));
    int const new_w(gdk_pixbuf_get_width(new_scaled));
    int const old_h(gdk_pixbuf_get_height(old_scaled));
    int const new_h(gdk_pixbuf_get_height(new_scaled));
    if (old_w!=new_w || old_h!=new_h)
    {
      // remove the old..
      GtkTextIter old_end (*iter);
      gtk_text_iter_forward_to_tag_toggle (&old_end, apply_tag);
      gtk_text_buffer_delete (buf, iter, &old_end);

      // insert the new..
      gtk_text_buffer_insert_pixbuf (buf, iter, new_scaled);
      gtk_text_buffer_insert (buf, iter, "\n", -1);
      set_pixbuf_at_offset (buf, begin_offset, original, new_scaled);

      // and apply the tag.
      GtkTextIter begin (*iter);
      gtk_text_iter_set_offset (&begin, begin_offset);
      gtk_text_buffer_apply_tag (buf, apply_tag, &begin, iter);
    }

    g_object_unref (new_scaled);
  }
}

gboolean
BodyPane :: mouse_button_pressed_cb (GtkWidget *w, GdkEventButton *e, gpointer p)
{
  return static_cast<BodyPane*>(p)->mouse_button_pressed (w, e);
}

gboolean
BodyPane :: mouse_button_pressed (GtkWidget *w, GdkEventButton *event)
{
  g_return_val_if_fail (GTK_IS_TEXT_VIEW(w), false);

  if (event->button==1 || event->button==2) {
      std::string const &url(_hover_url);
      if (! url.empty())
      {
        /* this is a crude way of making sure that double-click
         * doesn't open two or three browser windows. */
        static time_t last_url_time(0);
        const time_t this_url_time(time(nullptr));
        if (this_url_time != last_url_time)
        {
          last_url_time = this_url_time;
          URL ::open(_prefs, url.c_str());
        }
    } else { // maybe we're zooming in/out on a pic...
      GtkTextIter iter;
      GtkTextTag * pix_tag (get_named_tag_from_view (w, "pixbuf"));
      get_iter_from_event_coords (w, (int)event->x, (int)event->y, &iter);
      if (gtk_text_iter_has_tag (&iter, pix_tag))
      {
        if (!gtk_text_iter_starts_tag (&iter, pix_tag))
          gtk_text_iter_backward_to_tag_toggle (&iter, pix_tag);
        g_assert (gtk_text_iter_starts_tag (&iter, pix_tag));

        // percent_x,percent_y reflect where the user clicked in the picture
        GdkRectangle rec;
        gtk_text_view_get_iter_location (GTK_TEXT_VIEW(w), &iter, &rec);
        int buf_x, buf_y;
        gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW(w), GTK_TEXT_WINDOW_WIDGET,
                                               (gint)event->x, (gint)event->y, &buf_x, &buf_y);
        double const percent_x = (buf_x - rec.x) / (double)rec.width;
        double const percent_y = (buf_y - rec.y) / (double)rec.height;

        // resize the picture and refresh `iter'
        int const offset(gtk_text_iter_get_offset(&iter));
        GtkTextBuffer * buf (gtk_text_view_get_buffer (GTK_TEXT_VIEW(w)));
        bool const fullsize(toggle_fullsize_flag(buf));
        GtkAllocation aloc;
        gtk_widget_get_allocation(w, &aloc);
//        std::cerr<<"457 alloc "<<aloc.x<<" "<<aloc.y<<" "<<aloc.width<<" "<<aloc.height<<"\n";
        resize_picture_at_iter (buf, &iter, fullsize, &aloc, pix_tag);
        gtk_text_iter_set_offset (&iter, offset);
        set_cursor_from_iter (event->window, w, &iter);

        // x2,y2 are to position percent_x,percent_y in the middle of the window.
        GtkTextMark * mark = gtk_text_buffer_create_mark (buf, nullptr, &iter, true);
        double const x2 = CLAMP((percent_x + (percent_x - 0.5)), 0.0, 1.0);
        double const y2 = CLAMP((percent_y + (percent_y - 0.5)), 0.0, 1.0);
        gtk_text_view_scroll_to_mark (GTK_TEXT_VIEW(w), mark, 0.0, true, x2, y2);
        gtk_text_buffer_delete_mark (buf, mark);
      }
    }
  }
  return false;
}

/***
****  INIT
***/
namespace
{

GtkTextTag *get_or_create_tag(GtkTextTagTable *table, char const *key)
{
  g_assert(table);
  g_assert(key && *key);

  GtkTextTag *tag(gtk_text_tag_table_lookup(table, key));
  if (! tag)
  {
    tag = gtk_text_tag_new(key);
    gtk_text_tag_table_add(table, tag);
    g_object_unref(tag); // table refs it
  }
  return tag;
  }

  void set_text_buffer_tags(GtkTextBuffer *buffer, Prefs const &p)
  {
  PanColors const &colors(PanColors::get());
  const std::string fg(p.get_color_str("text-color-fg", colors.def_fg));
  const std::string bg(p.get_color_str("text-color-bg", colors.def_bg));

  GtkTextTagTable *table = gtk_text_buffer_get_tag_table(buffer);

  get_or_create_tag(table, "pixbuf");
  get_or_create_tag(table, "quote_0");


  g_object_set (get_or_create_tag (table, "text"),
      "foreground", fg.c_str(),
      "background", bg.c_str(),
      nullptr);
    g_object_set (get_or_create_tag(table,"bold"),
      "weight", PANGO_WEIGHT_BOLD,
      nullptr);
    g_object_set (get_or_create_tag (table, "italic"),
      "style", PANGO_STYLE_ITALIC,
      nullptr);
    g_object_set (get_or_create_tag (table, "underline"),
      "underline", PANGO_UNDERLINE_SINGLE,
      nullptr);
    g_object_set (get_or_create_tag (table, "url"),
      "underline", PANGO_UNDERLINE_SINGLE,
      "foreground", p.get_color_str ("body-pane-color-url", TANGO_SKY_BLUE_DARK).c_str(),
      "background", p.get_color_str ("body-pane-color-url-bg", bg).c_str(),
      nullptr);
    g_object_set (get_or_create_tag (table, "quote_1"),
      "foreground", p.get_color_str ("body-pane-color-quote-1", TANGO_CHAMELEON_DARK).c_str(),
      "background", p.get_color_str ("body-pane-color-quote-1-bg", bg).c_str(),
      nullptr);
    g_object_set (get_or_create_tag (table, "quote_2"),
      "foreground", p.get_color_str ("body-pane-color-quote-2", TANGO_ORANGE_DARK).c_str(),
      "background", p.get_color_str ("body-pane-color-quote-2-bg", bg).c_str(),
      nullptr);
    g_object_set (get_or_create_tag (table, "quote_3"),
      "foreground", p.get_color_str ("body-pane-color-quote-3", TANGO_PLUM_DARK).c_str(),
      "background", p.get_color_str ("body-pane-color-quote-3-bg", bg).c_str(),
      nullptr);
    g_object_set (get_or_create_tag (table, "signature"),
      "foreground", p.get_color_str ("body-pane-color-signature", TANGO_SKY_BLUE_LIGHT).c_str(),
      "background", p.get_color_str ("body-pane-color-signature-bg", bg).c_str(),
      nullptr);
  }
}

/***
****
***/
namespace
{
  // handle up, down, pgup, pgdown to scroll
  gboolean text_key_pressed (GtkWidget * w, GdkEventKey * event, gpointer scroll)
  {
    gboolean handled (false);

    g_return_val_if_fail (GTK_IS_TEXT_VIEW(w), false);

    const bool up = event->keyval==GDK_KEY_Up || event->keyval==GDK_KEY_KP_Up;
    const bool down = event->keyval==GDK_KEY_Down || event->keyval==GDK_KEY_KP_Down;

    if (up || down)
    {
      handled = true;
      gtk_text_view_place_cursor_onscreen (GTK_TEXT_VIEW(w));
      GtkAdjustment * adj = gtk_scrolled_window_get_vadjustment (
                                                      GTK_SCROLLED_WINDOW(scroll));
      gdouble val = gtk_adjustment_get_value(adj);
      if (up)
        val -= gtk_adjustment_get_step_increment(adj);
      else
        val += gtk_adjustment_get_step_increment(adj);
      val = MAX(val, gtk_adjustment_get_lower(adj) );
      val = MIN(val, gtk_adjustment_get_upper(adj) - gtk_adjustment_get_page_size(adj) );
      gtk_adjustment_set_value (adj, val);
    }

    return handled;
  }
}

/****
*****
*****   SETTING THE TEXT FROM AN ARTICLE
*****
****/
namespace
{
  bool text_get_show_all_headers () { return false; }

  /**
   * Returns the quote tag ("quote_0", "quote_1", etc) appropriate for the line.
   * The right tag is calculated by adding up the number of quote characters
   * at the beginning of the line.
   *
   * @param utf8_line the line whose quote status we're checking
   * @param utf8_byte_len the byte length of utf8_line
   * @return a const string for the line's quote tag.  Never nullptr.
   */
  const char * get_quote_tag (const TextMassager     * text_massager,
                              const char             * utf8_line,
                              int                      utf8_byte_len)
  {
    const char * str = utf8_line;
    const char * retval = "quote_0";

    if (0<utf8_byte_len && str && *str)
    {
      const char * line_end = utf8_line + utf8_byte_len;
      int depth = 0;

      // walk past leading spaces
      while (str!=line_end && g_unichar_isspace(g_utf8_get_char(str)))
        str = g_utf8_next_char (str);

      // count the number of spaces or quote characters
      for (;;) {
        if (str == line_end)
          break;
        else if (text_massager->is_quote_character (g_utf8_get_char (str)))
          ++depth;
        else if (!g_unichar_isspace(g_utf8_get_char(str)))
          break;
        str = g_utf8_next_char (str);
      }

      if (depth != 0)
      {
        switch (depth % 3) {
          case 1: retval = "quote_1"; break;
          case 2: retval = "quote_2"; break;
          case 0: retval = "quote_3"; break;
        }
      }
    }

    return retval;
  }

  typedef std::map<std::string,GdkPixbuf*> pixbufs_t;

  // don't use this directly -- use get_emoticons()
  pixbufs_t emoticon_pixbufs;

  void free_emoticons ()
  {
    foreach_const (pixbufs_t, emoticon_pixbufs, it)
      g_object_unref (it->second);
  }

  pixbufs_t& get_emoticons ()
  {
     return emoticon_pixbufs;
  }

 void create_emoticons()
  {
    emoticon_pixbufs[":)"] = load_icon("icon_mozilla_smile.png");
    emoticon_pixbufs[":-)"] = load_icon("icon_mozilla_smile.png");
    emoticon_pixbufs[";)"] = load_icon("icon_mozilla_wink.png");
    emoticon_pixbufs[":("] = load_icon("icon_mozilla_frown.png");
    emoticon_pixbufs[":P"] = load_icon("icon_mozilla_tongueout.png");
    emoticon_pixbufs[":O"] = load_icon("icon_mozilla_surprised.png");
  }

  enum TagMode { ADD, REPLACE };

  void
  set_section_tag (GtkTextBuffer     * buffer,
                   GtkTextIter       * start,
                   const StringView  & body,
                   const StringView  & area,
                   const char        * tag,
                   TagMode             mode)
  {
    // if no alnums, chances are area is a false positive
    int alnums (0);
    for (const char *pch(area.begin()), *end(area.end()); pch!=end; ++pch)
      if (::isalnum(*pch))
        ++alnums;
    if (!alnums)
      return;

    GtkTextIter mark_start = *start;
    gtk_text_iter_forward_chars (&mark_start,
                                 g_utf8_strlen(body.str, area.str-body.str));
    GtkTextIter mark_end = mark_start;
    gtk_text_iter_forward_chars (&mark_end, g_utf8_strlen(area.str,area.len));
    if (mode == REPLACE)
      gtk_text_buffer_remove_all_tags (buffer, &mark_start, &mark_end);
    gtk_text_buffer_apply_tag_by_name (buffer, tag, &mark_start, &mark_end);
  }

  void
  show_signature (GtkTextBuffer      * buffer,
                    GtkTextMark        * mark,
                    std::string        & body,
                    bool                show)
  {
    GtkTextTagTable * tags (gtk_text_buffer_get_tag_table (buffer));
    if (tags)
    {
      GtkTextTag * sig_tag (gtk_text_tag_table_lookup (tags, "signature"));
      if (sig_tag)
        g_object_set (sig_tag, "invisible", !show, nullptr);
    }
  }

  void
  replace_emoticon_text_with_pixbuf (GtkTextBuffer      * buffer,
                                     GtkTextMark        * mark,
                                     std::string        & body,
                                     const std::string  & text,
                                     GdkPixbuf          * pixbuf)
  {
    g_assert (!text.empty());
    g_assert (pixbuf != nullptr);

    GtkTextTagTable * tags (gtk_text_buffer_get_tag_table (buffer));
    GtkTextTag * url_tag (gtk_text_tag_table_lookup (tags, "url"));

    const size_t n (text.size());
    std::string::size_type pos (0);
    while (((pos=body.find(text,pos))) != body.npos)
    {
      GtkTextIter begin;
      gtk_text_buffer_get_iter_at_mark (buffer, &begin, mark);
      gtk_text_iter_forward_chars (&begin, g_utf8_strlen(&body[0],pos));

      if (gtk_text_iter_has_tag (&begin, url_tag))
        pos += n;
      else {
        GtkTextIter end (begin);
        gtk_text_iter_forward_chars (&end, n);
        gtk_text_buffer_delete (buffer, &begin, &end);
        body.erase (pos, text.size());
        gtk_text_buffer_insert_pixbuf (buffer, &end, pixbuf);
        body.insert (pos, 1, '?'); // make body.size() match the textbuf's size
      }
    }
  }

  /**
   * Appends the specified body into the text buffer.
   * This function takes care of muting quotes and marking
   * quoted and URL areas in the GtkTextBuffer.
   */
  void
  append_text_buffer_nolock (const TextMassager  * text_massager,
                             GtkTextBuffer       * buffer,
                             const StringView    & body_in,
                             bool                  mute_quotes,
                             bool                  show_smilies,
                             bool                  show_sig,
                             bool                  do_markup,
                             bool                  do_urls)
  {

    g_return_if_fail (buffer!=nullptr);
    g_return_if_fail (GTK_IS_TEXT_BUFFER(buffer));

    // mute the quoted text, if desired
    std::string body;
    if (mute_quotes)
      body = text_massager->mute_quotes (body_in);
    else
      body.assign (body_in.str, body_in.len);

    // insert the text
    GtkTextIter end;
    gtk_text_buffer_get_end_iter (buffer, &end);
    GtkTextMark * mark = gtk_text_buffer_create_mark (buffer, "blah", &end, true);
    gtk_text_buffer_insert (buffer, &end, body.c_str(), -1);
    GtkTextIter start;
    gtk_text_buffer_get_iter_at_mark (buffer, &start, mark);

    StringView v(body), line;
    GtkTextIter mark_start (start);

    // colorize text
    gtk_text_buffer_apply_tag_by_name (buffer, "text", &mark_start, &end);

    // find where the signature begins...
    const char * sig_point (nullptr);
    int offset (0);
    if (GNKSA::find_signature_delimiter (v, offset) != GNKSA::SIG_NONE)
      sig_point = v.str + offset;

    // colorize the quoted text
    GtkTextIter mark_end;
    std::string last_quote_tag;
    bool is_sig (false);
    const char * last_quote_begin (v.str);
    while (v.pop_token (line, '\n'))
    {
      if (line.empty())
        continue;

      if (line.str == sig_point)
        is_sig = true;

      const std::string quote_tag = is_sig
        ?  "signature"
        : get_quote_tag (text_massager, line.str, line.len);

      // if we've changed tags, colorize the previous block
      if (!last_quote_tag.empty() && quote_tag!=last_quote_tag) {
        mark_end = mark_start;
        gtk_text_iter_forward_chars (&mark_end, g_utf8_strlen(last_quote_begin,line.str-1-last_quote_begin));
        gtk_text_buffer_apply_tag_by_name (buffer, last_quote_tag.c_str(), &mark_start, &mark_end);
        mark_start = mark_end;
        gtk_text_iter_forward_chars (&mark_start, 1);
        last_quote_begin = line.str;
      }

      last_quote_tag = quote_tag;
    }

    // apply the final tag, if there is one */
    if (!last_quote_tag.empty()) {
      gtk_text_buffer_get_end_iter (buffer, &mark_end);
      gtk_text_buffer_apply_tag_by_name (buffer, last_quote_tag.c_str(), &mark_start, &mark_end);
    }

    // apply markup
    const StringView v_all (body);
    if (do_markup)
    {
      bool is_patch = false;
      std::set<const char*> already_processed;
      StringView v(body), line;
      while (v.pop_token (line, '\n'))
      {
        // if we detect that this message contains a patch,
        // turn off markup for the rest of the article.
        if (!is_patch)
          is_patch = (line.strstr("--- ")==line.str) || (line.strstr("@@ ")==line.str);
        if (is_patch)
          continue;

        for (;;)
        {
          // find the first markup character.
          // if we've already used it,
          // or if it was preceeded by something not a space or punctuation,
          // then keep looking.
          const char * b (line.strpbrk ("_*/"));
          if (!b)
            break;
          if (already_processed.count(b) ||
              (b!=&line.front() && !isspace(b[-1]) && !ispunct(b[-1]))) {
            line.eat_chars (b+1-line.str);
            continue;
          }

          // find the ending corresponding markup character.
          // if it was followed by something not a space or punctuation, keep looking.
          const char * e = b;
          while ((e = line.strchr (*b, 1+(e-line.str)))) {
            if (e==&line.back() || isspace(e[1]) || ispunct(e[1]) || strchr("_*/", e[1]))
              break;
          }

          if (e) {
            already_processed.insert (e);
            const char * type (nullptr);
            switch (*b) {
              case '*': type = "bold"; break;
              case '_': type = "underline"; break;
              case '/': type = "italic"; break;
            }
            set_section_tag (buffer, &start, v_all, StringView(b,e+1), type, ADD);
          }

          line.eat_chars (b+1-line.str);
        }
      }
    }

    // colorize urls
    if (do_urls) {
      StringView area;
      StringView march (v_all);
      while ((url_find (march, area))) {
        // Do no highlight URL in signature when not showing signature
        if (area.str > sig_point && !show_sig)
          break;
        set_section_tag (buffer, &start, v_all, area, "url", REPLACE);
        march = march.substr (area.str + area.len, nullptr);
      }
    }

    // do this last, since it alters the text instead of just marking it up
    if (show_smilies) {
      pixbufs_t& emoticons (get_emoticons());
      foreach_const (pixbufs_t, emoticons, it)
        replace_emoticon_text_with_pixbuf (buffer, mark, body, it->first, it->second);
    }

    show_signature (buffer, mark, body, show_sig);

    gtk_text_buffer_delete_mark (buffer, mark);
  }

  /**
   * Generates a GtkPixmap object from a given GMimePart that contains an image.
   * Used for displaying attached pictures inline.
   */
  GdkPixbuf* get_pixbuf_from_gmime_part (GMimePart * part)
  {
    GdkPixbufLoader * l (gdk_pixbuf_loader_new ());
    GError * err (nullptr);

    // populate the loader
    GMimeDataWrapper * wrapper (g_mime_part_get_content (part));
    if (wrapper)
    {
      GMimeStream * mem_stream (g_mime_stream_mem_new ());
      g_mime_data_wrapper_write_to_stream (wrapper, mem_stream);
      GByteArray * buffer (GMIME_STREAM_MEM(mem_stream)->buffer);
      gsize bytesLeft = buffer->len;
      guchar * data = buffer->data;

      // ticket #467446 - workaround gdkpixbuf <= 2.12.x's
      // jpg loader bug (#494667) by feeding the loader in
      // smaller chunks
      while( bytesLeft > 0 )
      {
          const gsize n = MIN( 4096, bytesLeft );

          gdk_pixbuf_loader_write (l, data, n, &err);
          if (err) {
            Log::add_err (err->message);
            g_clear_error (&err);
            break;
          }

          bytesLeft -= n;
          data += n;
      }

      g_object_unref (mem_stream);
    }

    // flush the loader
    gdk_pixbuf_loader_close (l, &err);
    if (err) {
      Log::add_err (err->message);
      g_clear_error (&err);
    }

    // create the pixbuf
    GdkPixbuf * pixbuf (nullptr);
    if (!err)
      pixbuf = gdk_pixbuf_loader_get_pixbuf (l);
    else {
      Log::add_err (err->message);
      g_clear_error (&err);
    }

    // cleanup
    if (pixbuf)
      g_object_ref (G_OBJECT(pixbuf));
    g_object_unref (G_OBJECT(l));
    return pixbuf;
  }

}

void
BodyPane :: append_part (GMimeObject * parent, GMimeObject * obj, GtkAllocation * widget_size)
{
  bool is_done (false);

  // we only need leaf parts..
  if (!GMIME_IS_PART (obj))
    return;

  GMimePart * part = GMIME_PART (obj);
  GMimeContentType * type = g_mime_object_get_content_type (GMIME_OBJECT (part));

  // decide whether or not this part is a picture
  bool is_image (g_mime_content_type_is_type (type, "image", "*"));
  if (!is_image && g_mime_content_type_is_type(type, "application", "octet-stream")) {
    const char *type, *subtype;
    mime::guess_part_type_from_filename(g_mime_part_get_filename(part), &type, &subtype);
    is_image = type && !strcmp(type,"image");
  }

  // if it's a picture, draw it
  if (is_image)
  {
    GdkPixbuf * original (get_pixbuf_from_gmime_part (part));
    const bool fullsize (!_prefs.get_flag ("size-pictures-to-fit", true));
    GdkPixbuf * scaled (size_to_fit (original, fullsize ? nullptr : widget_size));

    if (scaled != nullptr)
    {
      GtkTextIter iter;

      // if this is the first thing in the buffer, precede it with a linefeed.
      gtk_text_buffer_get_end_iter (_buffer, &iter);
      if (gtk_text_buffer_get_char_count (_buffer) > 0)
        gtk_text_buffer_insert (_buffer, &iter, "\n", -1);

      // rembember the location of the first picture.
      if (gtk_text_buffer_get_mark (_buffer, FIRST_PICTURE) == nullptr)
        gtk_text_buffer_create_mark (_buffer, FIRST_PICTURE, &iter, true);
        gtk_text_buffer_create_mark (_buffer, FIRST_PICTURE, &iter, true);

      // add the picture
      const int begin_offset (gtk_text_iter_get_offset (&iter));
      gtk_text_buffer_insert_pixbuf (_buffer, &iter, scaled);
      gtk_text_buffer_insert (_buffer, &iter, "\n", -1);
      GtkTextIter iter_begin (iter);
      gtk_text_iter_set_offset (&iter_begin, begin_offset);

      // hook onto the tag a reference to the original picture
      // so that we can resize it later if user resizes the text pane.
      set_pixbuf_at_offset (_buffer, begin_offset, original, scaled);
      set_fullsize_flag (_buffer, fullsize);

      GtkTextTagTable * tags (gtk_text_buffer_get_tag_table (_buffer));
      GtkTextTag * tag (gtk_text_tag_table_lookup (tags, "pixbuf"));
      gtk_text_buffer_apply_tag (_buffer, tag, &iter_begin, &iter);

      g_object_unref (scaled);
      g_object_unref (original);

      is_done = true;
    }
  }

  // or, if it's text, display it
  else if (g_mime_content_type_is_type (type, "text", "*"))
  {
    const char * fallback_charset (_charset.c_str());
    const char * p_flowed (g_mime_object_get_content_type_parameter(obj,"format"));
    const bool flowed (g_strcmp0 (p_flowed, "flowed") == 0);
    std::string str = mime_part_to_utf8 (part, fallback_charset);

    if (!str.empty() && _prefs.get_flag ("wrap-article-body", false))
      str = _tm.fill (str, flowed);

    const bool do_mute (_prefs.get_flag ("mute-quoted-text", false));
    const bool do_smilies (_prefs.get_flag ("show-smilies-as-graphics", true));
    const bool do_markup (_prefs.get_flag ("show-text-markup", true));
    const bool do_urls (_prefs.get_flag ("highlight-urls", true));
    const bool do_sig(_prefs.get_flag ("show-article-sig", true));
    append_text_buffer_nolock (&_tm, _buffer, str, do_mute, do_smilies, do_sig, do_markup, do_urls);
    is_done = true;
  }

  // picture or otherwise, add to list of attachments
  if (is_image || !is_done) {
    const char * filename = g_mime_part_get_filename (part);
    char * pch = (filename && *filename)
      ? g_strdup_printf ("%s", filename)
      : g_strdup_printf (_("Unnamed File"));

    add_attachment_to_toolbar (pch);

    _attach_names.insert(pch);
  }
}

void
BodyPane :: foreach_part_cb (GMimeObject* parent, GMimeObject* o, gpointer self)
{
  BodyPane * pane = static_cast<BodyPane*>(self);
  GtkWidget * w (pane->_text);
  GtkAllocation aloc;
  gtk_widget_get_allocation(w, &aloc);
  pane->append_part (parent, o, &aloc);
}


/***
****  HEADERS
***/
namespace
{
  void add_bold_header_value (std::string   & s,
                              GMimeMessage  * message,
                              const char    * key,
                              const char    * fallback_charset)
  {
    const char * val (message ? g_mime_object_get_header ((GMimeObject *)message, key) : "");
    const std::string utf8_val (header_to_utf8 (val, fallback_charset));
    char * e (nullptr);
    if (strcmp (key, "From"))
      e = g_markup_printf_escaped ("<span weight=\"bold\">%s</span>", utf8_val.c_str());
    else {
      const StringView v = GNKSA :: get_short_author_name (utf8_val);
      e = g_markup_printf_escaped ("<span weight=\"bold\">%s</span>", v.to_string().c_str());
    }
    s += e;
    g_free (e);
  }

  size_t add_header_line (std::string  & s,
                          const char   * key_i18n,
                          const char   * key,
                          const char   * val,
                          const char   * fallback_charset)
  {
    char * e;
    e = g_markup_printf_escaped ("<span weight=\"bold\">%s:</span> ", key_i18n);
    s += e;
    g_free (e);
    const std::string utf8_val (header_to_utf8 (val, fallback_charset));
    e = g_markup_printf_escaped ("%s\n", utf8_val.c_str());
    s += e;
    const size_t retval (g_utf8_strlen(key,-1) + g_utf8_strlen(utf8_val.c_str(),-1) + 2);
    g_free (e);

    return retval;
  }

  size_t add_header_line (std::string   & s,
                          GMimeMessage  * msg,
                          const char    * key_i18n,
                          const char    * key,
                          const char    * fallback_charset)
  {
    const char * val (msg ? g_mime_object_get_header ((GMimeObject *) msg, key) : "");
    return add_header_line (s, key_i18n, key, val, fallback_charset);
  }

}

void
BodyPane :: set_text_from_message (GMimeMessage * message)
{
  const char * fallback_charset (_charset.empty() ? nullptr : _charset.c_str());

  // mandatory headers...
  std::string s;
  size_t w(0), l(0);
  l = add_header_line (s, message, _("Subject"), "Subject", fallback_charset);
  w = std::max (w, l);
  l = add_header_line (s, message, _("From"), "From", fallback_charset);
  w = std::max (w, l);
  l = add_header_line (s, message, _("Date"), "Date", fallback_charset);
  w = std::max (w, l);

  // conditional headers...
  if (message) {
    const StringView newsgroups (g_mime_object_get_header ((GMimeObject *) message, "Newsgroups"));
    if (newsgroups.strchr(',')) {
      l = add_header_line (s, message, _("Newsgroups"), "Newsgroups", fallback_charset);
      w = std::max (w, l);
    }
	const StringView user_agent(
			g_mime_object_get_header((GMimeObject *) message,
					"User-Agent"));
	if (!user_agent.empty()) {
		l = add_header_line(s, message, _("User-Agent"), "User-Agent",
				fallback_charset);
		w = std::max(w, l);
	}
	const StringView x_newsreader(
			g_mime_object_get_header((GMimeObject *) message,
					"X-Newsreader"));
	if (!x_newsreader.empty()) {
		l = add_header_line(s, message, _("User-Agent"), "X-Newsreader",
				fallback_charset);
		w = std::max(w, l);
	}
	const StringView x_mailer(
			g_mime_object_get_header((GMimeObject *) message, "X-Mailer"));
	if (!x_mailer.empty()) {
		l = add_header_line(s, message, _("User-Agent"), "X-Mailer",
				fallback_charset);
		w = std::max(w, l);
	}
    const StringView followup_to (g_mime_object_get_header ((GMimeObject *) message, "Followup-To"));
    if (!followup_to.empty() && (followup_to!=newsgroups)) {
      l = add_header_line (s, message, _("Followup-To"), "Followup-To", fallback_charset);
      w = std::max (w, l);
    }
    const StringView reply_to (g_mime_object_get_header ((GMimeObject *) message, "Reply-To"));
    if (!reply_to.empty()) {
      const StringView from (g_mime_object_get_header ((GMimeObject *) message, "From"));
      StringView f_addr, f_name, rt_addr, rt_name;
      GNKSA :: do_check_from (from, f_addr, f_name, false);
      GNKSA :: do_check_from (reply_to, rt_addr, rt_name, false);
      if (f_addr != rt_addr) {
        l = add_header_line (s, message, _("Reply-To"), "Reply-To", fallback_charset);
        w = std::max (w, l);
      }
    }
  }

  s.resize (std::max((size_t)0,s.size()-1)); // remove trailing linefeed
  gtk_label_set_markup (GTK_LABEL(_headers), s.c_str());


  // set the x-face...
  gtk_image_clear(GTK_IMAGE(_xface));
  const char * pch = message ? g_mime_object_get_header ((GMimeObject *) message, "X-Face") : nullptr;
  if (pch && gtk_widget_get_window(_xface) )
  {
    gtk_widget_set_size_request (_xface, FACE_SIZE, FACE_SIZE);
    GdkPixbuf *pixbuf = nullptr;
    pixbuf = pan_gdk_pixbuf_create_from_x_face (pch);
    gtk_image_set_from_pixbuf (GTK_IMAGE(_xface), pixbuf);
    g_object_unref (pixbuf);
  } else {
    gtk_widget_set_size_request(_xface, 0, FACE_SIZE);
  }

  // set the face
  gtk_image_clear(GTK_IMAGE(_face));
  pch = message ? g_mime_object_get_header ((GMimeObject *) message, "Face") : nullptr;
  if (pch && gtk_widget_get_window(_face))
  {
    gtk_widget_set_size_request (_face, FACE_SIZE, FACE_SIZE);
    GMimeEncoding dec;
    g_mime_encoding_init_decode(&dec, GMIME_CONTENT_ENCODING_BASE64);
    guchar* buf = new guchar[strlen(pch)];
    int len = g_mime_encoding_step(&dec, pch, strlen(pch), (char*)buf);
    GdkPixbufLoader *pl = gdk_pixbuf_loader_new_with_type( "png", nullptr);
    gdk_pixbuf_loader_write(pl, buf, len, nullptr);
    gdk_pixbuf_loader_close(pl, nullptr);
    GdkPixbuf *pixbuf = gdk_pixbuf_loader_get_pixbuf(pl);
    gtk_image_set_from_pixbuf (GTK_IMAGE(_face), pixbuf);
    g_object_unref(pl);
    delete[] buf;
  } else {
    gtk_widget_set_size_request(_face, 0, FACE_SIZE);
  }

  // set the terse headers...
  s.clear ();
  add_bold_header_value (s, message, "Subject", fallback_charset);
  s += _(" from ");
  add_bold_header_value (s, message, "From", fallback_charset);
  s += _(" at ");
  add_bold_header_value (s, message, "Date", fallback_charset);
  gtk_label_set_markup (GTK_LABEL(_terse), s.c_str());
  // ellipsize mode is useless w/o this in expander...
  gtk_label_set_width_chars (GTK_LABEL(_terse), (int)s.size());

  // clear the text buffer...
  GtkTextIter start, end;
  gtk_text_buffer_get_bounds (_buffer, &start, &end);
  gtk_text_buffer_delete (_buffer, &start, &end);
  if (gtk_text_buffer_get_mark (_buffer, FIRST_PICTURE) != nullptr)
    gtk_text_buffer_delete_mark_by_name (_buffer, FIRST_PICTURE);
  clear_pixbuf_cache (_buffer);

  // maybe add the headers
  const bool do_show_headers (_prefs.get_flag ("show-all-headers", false));
  if (message && do_show_headers) {
    char * headers (g_mime_object_get_headers ((GMimeObject *) message, nullptr));
    GtkTextIter end;
    gtk_text_buffer_get_end_iter (_buffer, &end);
    StringView line, v(headers);
    while (v.pop_token (line, '\n')) {
      const std::string h (header_to_utf8 (line, fallback_charset));
      gtk_text_buffer_insert (_buffer, &end, h.c_str(), h.size());
      gtk_text_buffer_insert (_buffer, &end, "\n", 1);
    }
    gtk_text_buffer_insert (_buffer, &end, "\n", 1);
    g_free (headers);
  }

  clear_attachments();

  // FIXME: need to set a mark here so that when user hits follow-up,
  // the all-headers don't get included in the followup

  // set the text buffer...
  if (message)
    g_mime_message_foreach (message, foreach_part_cb, this);
  // set the html view
  GtkTextBuffer* buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW(_text));
  GtkTextIter _start, _end;
  gtk_text_buffer_get_bounds (buffer, &_start, &_end);
#ifdef HAVE_WEBKIT
  char* buf (gtk_text_buffer_get_text (buffer, &_start, &_end, false));
  if (buf) set_html_text(buf);
#endif

  // if there was a picture, scroll to it.
  // otherwise scroll to the top of the body.
  GtkTextMark * mark = gtk_text_buffer_get_mark (_buffer, FIRST_PICTURE);
  if (mark && _prefs.get_flag ("focus-on-image", true))
    gtk_text_view_scroll_to_mark (GTK_TEXT_VIEW(_text), mark, 0.0, true, 0.0, 0.0);
  else {
    GtkTextIter iter;
    gtk_text_buffer_get_start_iter  (_buffer, &iter);
    gtk_text_view_scroll_to_iter (GTK_TEXT_VIEW(_text), &iter, 0.0, true, 0.0, 0.0);
  }
}

void
BodyPane :: refresh ()
{
  set_text_from_message (_message);
}

#ifdef HAVE_GMIME_CRYPTO
gboolean
BodyPane :: on_verbose_tooltip_cb(GtkWidget  *widget,
                                  gint        x,
                                  gint        y,
                                  gboolean    keyboard_tip,
                                  GtkTooltip *tooltip,
                                  gpointer    data)
{
  BodyPane* pane = static_cast<BodyPane*>(data);
  if (!pane) return false;

  gtk_tooltip_set_icon_from_stock (tooltip, GTK_STOCK_DIALOG_INFO, GTK_ICON_SIZE_DIALOG);

  GPGDecErr& err = pane->_gpgerr;
  GPGSignersInfo& info = err.signers;
  if (err.no_sigs) return false;
  if (info.signers.empty()) return false;

  EvolutionDateMaker ed;

  char buf[2048];
  std::pair<std::string,std::string> name_and_email = get_email_address(info.signers[0].name);
  g_snprintf(buf, sizeof(buf),
             _("<u>This is a <b>PGP-Signed</b> message.</u>\n\n"
             "<b>Signer:</b> %s ('%s')\n"
             "<b>Valid until:</b> %s\n"
             "<b>Created on:</b> %s"),
             name_and_email.first.c_str(), name_and_email.second.c_str(),
             (info.signers[0].never_expires ? _("always") : ed.get_date_string(info.signers[0].expires)),
             ed.get_date_string(info.signers[0].created)
             );

  gtk_tooltip_set_markup (tooltip, buf);

  return true;
}
#endif

void
BodyPane :: set_article (const Article& a)
{
  _article = a;

  if (_message)
    g_object_unref (_message);
#ifdef HAVE_GMIME_CRYPTO
  _gpgerr.clear();
  _message = _cache.get_message (_article.get_part_mids(), _gpgerr);
#else
  _message = _cache.get_message (_article.get_part_mids());
#endif

  refresh ();

  set_cleared(false);

  _data.mark_read (_article);
}

void
BodyPane :: clear ()
{
  if (_message)
    g_object_unref (_message);
  _message = nullptr;

  set_cleared(true);

  refresh ();
}

void
BodyPane :: select_all ()
{
  GtkTextIter start, end;
  gtk_text_buffer_get_bounds (_buffer, &start, &end);
  gtk_text_buffer_select_range (_buffer, &start, &end);
}

void
BodyPane :: rot13_selected_text ()
{
  GtkTextIter start, end;
  if (gtk_text_buffer_get_selection_bounds (_buffer, &start, &end))
  {
    // replace the range with a rot13'ed copy.
    gchar* pch = gtk_text_buffer_get_text (_buffer, &start, &end, false);
    const size_t len = strlen (pch);
    TextMassager :: rot13_inplace (pch);
    gtk_text_buffer_delete (_buffer, &start, &end);
    gtk_text_buffer_insert (_buffer, &end, pch, len);
    g_free (pch);

    // resync selection.
    // since gtk_text_buffer_insert() invalided start, we rebuild it first.
    start = end;
    gtk_text_iter_backward_chars (&start, len);
    gtk_text_buffer_select_range (_buffer, &start, &end);
  }
}

/***
****
***/

gboolean
BodyPane :: expander_activated_idle (gpointer self_gpointer)
{
  BodyPane *  self (static_cast<BodyPane*>(self_gpointer));
  GtkExpander * ex (GTK_EXPANDER(self->_expander));
  const bool expanded = gtk_expander_get_expanded (ex);
  gtk_expander_set_label_widget (ex, expanded ? self->_verbose : self->_terse);
  self->_prefs.set_flag ("body-pane-headers-expanded", expanded);
  return false;
}

void
BodyPane :: expander_activated_cb (GtkExpander*, gpointer self_gpointer)
{
  g_idle_add (expander_activated_idle, self_gpointer);
}

void
BodyPane :: verbose_clicked_cb (GtkWidget* w, GdkEvent  *event, gpointer self_gpointer)
{
  BodyPane *  self (static_cast<BodyPane*>(self_gpointer));
  GtkExpander * ex (GTK_EXPANDER(self->_expander));
  gtk_expander_set_expanded (ex, !gtk_expander_get_expanded (ex));
  g_idle_add (expander_activated_idle, self_gpointer);
}

void
BodyPane :: refresh_scroll_visible_state ()
{
  GtkScrolledWindow * w (GTK_SCROLLED_WINDOW (_scroll));
  GtkAdjustment * adj = gtk_scrolled_window_get_hadjustment (w);
  _hscroll_visible = gtk_adjustment_get_page_size(adj) < gtk_adjustment_get_upper(adj);
  adj = gtk_scrolled_window_get_vadjustment (w);
  _vscroll_visible = gtk_adjustment_get_page_size(adj) < gtk_adjustment_get_upper(adj);
}

// show*_cb exists to ensure that _hscroll_visible and _vscroll_visible
// are initialized properly when the body pane becomes shown onscreen.

gboolean
BodyPane :: show_idle_cb (gpointer pane)
{
  static_cast<BodyPane*>(pane)->refresh_scroll_visible_state ();
  return false;
}
void
BodyPane :: show_cb (GtkWidget* w G_GNUC_UNUSED, gpointer pane)
{
  g_idle_add (show_idle_cb, pane);
}

namespace
{
  guint text_size_allocated_idle_tag (0);
}

gboolean
BodyPane :: text_size_allocated_idle_cb (gpointer pane)
{
  static_cast<BodyPane*>(pane)->text_size_allocated_idle ();
  text_size_allocated_idle_tag = 0;
  return false;
}

void
BodyPane :: text_size_allocated_idle ()
{

  // prevent oscillation
  const bool old_h (_hscroll_visible);
  const bool old_v (_vscroll_visible);
  refresh_scroll_visible_state ();
  if ((old_h!=_hscroll_visible) || (old_v!=_vscroll_visible))
    return;

  // get the resize flag...
  GtkTextBuffer * buf (gtk_text_view_get_buffer (GTK_TEXT_VIEW(_text)));
  const bool fullsize (get_fullsize_flag (buf));

  // get the start point...
  GtkTextIter iter;
  gtk_text_buffer_get_start_iter (buf, &iter);

  // walk through the buffer looking for pictures to resize
  GtkTextTag * tag (get_named_tag_from_view (_text, "pixbuf"));
  GtkAllocation aloc;
  for (;;) {
    if (gtk_text_iter_begins_tag (&iter, tag))
    {
      gtk_widget_get_allocation(_text, &aloc);
//      std::cerr<<"1449 alloc "<<aloc.x<<" "<<aloc.y<<" "<<aloc.width<<" "<<aloc.height<<"\n";
      resize_picture_at_iter (buf, &iter, fullsize, &aloc, tag);
    }
    if (!gtk_text_iter_forward_char (&iter))
      break;
    if (!gtk_text_iter_forward_to_tag_toggle (&iter, tag))
      break;
  }
}

void
BodyPane :: text_size_allocated (GtkWidget     * text        G_GNUC_UNUSED,
                                 GtkAllocation * allocation  G_GNUC_UNUSED,
                                 gpointer        pane)
{
  if (!text_size_allocated_idle_tag)
       text_size_allocated_idle_tag = g_idle_add (text_size_allocated_idle_cb, pane);
}

/***
****
***/

void
BodyPane :: copy_url_cb (GtkMenuItem *mi G_GNUC_UNUSED, gpointer pane)
{
  static_cast<BodyPane*>(pane)->copy_url ();
}

void
BodyPane :: copy_url ()
{
  gtk_clipboard_set_text (gtk_clipboard_get (GDK_SELECTION_CLIPBOARD),
                          _hover_url.c_str(), _hover_url.size());
}

void
BodyPane :: populate_popup_cb (GtkTextView *v, GtkMenu *m, gpointer pane)
{
  static_cast<BodyPane*>(pane)->populate_popup(v, m);
}

void
BodyPane :: populate_popup (GtkTextView *v G_GNUC_UNUSED, GtkMenu *m)
{
  // menu separator comes first.
  GtkWidget * mi = gtk_menu_item_new();
  gtk_widget_show (mi);
  gtk_menu_shell_prepend (GTK_MENU_SHELL(m), mi);

  // then, on top of it, the suggestions menu.
  const bool copy_url_enabled = !_hover_url.empty();
  mi = gtk_menu_item_new_with_mnemonic (_("Copy _URL"));
  g_signal_connect (mi, "activate", G_CALLBACK(copy_url_cb), this);
  gtk_widget_set_sensitive (mi, copy_url_enabled);
  gtk_widget_show_all (mi);
  gtk_menu_shell_prepend (GTK_MENU_SHELL(m), mi);
}

namespace
{
  // This function can be triggered:
  //  - read a post with an attachment
  //  - in message body, bottom left, right click on the icon below "Attachments"
  static gboolean
  attachment_clicked_cb (GtkWidget *w, GdkEvent *event, gpointer p)
  {
    BodyPane * bp = static_cast<BodyPane*>(p);
    const gchar * fn = (char*)g_object_get_data (G_OBJECT(w), "filename");
    if (!fn) return TRUE;

    bp->_current_attachment = fn;

    if (event->type == GDK_BUTTON_PRESS && event->button.button == 3)
    {
      gtk_menu_popup_at_pointer(GTK_MENU(bp->_menu), event);
    }

    return TRUE;
  }
}

void
BodyPane :: menu_clicked (const MenuSelection& ms)
{
  std::vector<Article> copies;
  copies.push_back(_article);
  GroupPrefs _group_prefs;
  SaveAttachmentsDialog * dialog = new SaveAttachmentsDialog (
    _prefs, _group_prefs, _data, _data, _cache,
    _data, _queue, get_window(_root),
    _header_pane->get_group(), copies,
    ms == MENU_SAVE_ALL ? TaskArticle::SAVE_ALL : TaskArticle::SAVE_AS,
    _current_attachment);
  gtk_widget_show (dialog->root());
}

void
BodyPane :: menu_clicked_as_cb (GtkWidget* w, gpointer ptr)
{
  BodyPane* p = static_cast<BodyPane*>(ptr);
  if (!p) return;
  p->menu_clicked(MENU_SAVE_AS);
}

void
BodyPane :: menu_clicked_all_cb (GtkWidget* w, gpointer ptr)
{
  BodyPane* p = static_cast<BodyPane*>(ptr);
  if (!p) return;
  p->menu_clicked(MENU_SAVE_ALL);
}


GtkWidget*
BodyPane :: new_attachment (const char* filename)
{
  if (!filename) return nullptr;

  GtkWidget* w = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
  GtkWidget* attachment = gtk_label_new(filename);
  GtkWidget * image = gtk_image_new_from_stock(GTK_STOCK_FILE, GTK_ICON_SIZE_MENU);

  gtk_label_set_selectable (GTK_LABEL(attachment), true);

  GtkWidget *event_box = gtk_event_box_new ();
  gtk_container_add (GTK_CONTAINER (event_box), image);
  g_object_set_data (G_OBJECT(event_box), "filename", (gpointer)filename);

  gtk_box_pack_start (GTK_BOX(w), event_box, false, false, 0);
  gtk_box_pack_start (GTK_BOX(w), attachment, false, false, 0);

  g_signal_connect(event_box, "button_press_event", G_CALLBACK(attachment_clicked_cb), this);

  return w;
}


void
BodyPane :: clear_attachments()
{
  _cur_col = 0;
  _cur_row = 0;
  _attachments = 0;
  _current_attachment = nullptr;

  {
    gtk_widget_set_no_show_all (_att_box, TRUE);
    gtk_widget_hide (_att_box);
    gtk_container_remove (GTK_CONTAINER (_att_box), _att_toolbar);
    _att_toolbar = nullptr;
    (void)create_attachments_toolbar(_att_box);
  }

}


/// FIXME : shows only half the icon on gtk2+, gtk3+ works fine. hm....
void
BodyPane :: add_attachment_to_toolbar (const char* fn)
{
  if (!fn) return;

  GtkWidget* w = new_attachment(fn);
//  gtk_widget_set_size_request(w, -1, 32);

  ++_attachments;
  if (_attachments % 4 == 0 && _attachments != 0)
  {
    gtk_grid_insert_row (GTK_GRID(_att_toolbar), ++_cur_row);
    _cur_col = 0;
  }

  gtk_grid_attach (GTK_GRID(_att_toolbar), w, _cur_col++, _cur_row, 1, 1);

  gtk_widget_set_no_show_all (_att_box, FALSE);
  gtk_widget_show_all (_att_box);
}

#ifdef HAVE_WEBKIT
void
BodyPane :: set_html_text (const char* text)
{
  webkit_web_view_load_string (WEBKIT_WEB_VIEW (_web_view), text, nullptr, nullptr, "");
}
#endif

GtkWidget*
BodyPane :: create_attachments_toolbar (GtkWidget* box)
{

  _cur_col = 0;
  _cur_row = 0;

  GtkWidget * w;
  w = _att_toolbar = gtk_grid_new();
  gtk_grid_insert_row (GTK_GRID(w), 0);
  gtk_grid_set_column_spacing (GTK_GRID(w), 3);
  gtk_grid_set_row_spacing (GTK_GRID (w), 4);
  gtk_container_add (GTK_CONTAINER (box), w);

  return box;
}

/***
****
***/

BodyPane :: BodyPane (Data& data, ArticleCache& cache, Prefs& prefs, GroupPrefs & gp, Queue& q, HeaderPane* hp):
  _prefs (prefs),
  _group_prefs(gp),
  _queue(q),
  _header_pane(hp),
  _data (data),
  _cache (cache),
#ifdef HAVE_WEBKIT
  _web_view (webkit_web_view_new ()),
#endif
  _hscroll_visible (false),
  _vscroll_visible (false),
  _message (nullptr),
#ifdef HAVE_GMIME_CRYPTO
//  _gpgerr(GPG_DECODE),
#endif
  _attachments(0),
  _cleared (true),
  _current_attachment(nullptr)
{

  GtkWidget * w, * l, * hbox;

  for (guint i=0; i<NUM_ICONS; ++i)
    icons[i].pixbuf = load_icon (icons[i].pixbuf_file);

  create_cursors();
  create_emoticons();

  // menu for popup menu for attachments
  _menu = gtk_menu_new ();
  l = gtk_menu_item_new_with_label(_("Save Attachment As..."));
  g_signal_connect (l, "activate", G_CALLBACK(menu_clicked_as_cb), this);
  gtk_menu_shell_append (GTK_MENU_SHELL(_menu), l);
  l =  gtk_menu_item_new_with_label(_("Save All Attachments"));
  _selection = MENU_SAVE_ALL;
  g_signal_connect (l, "activate", G_CALLBACK(menu_clicked_all_cb), this);
  gtk_menu_shell_append (GTK_MENU_SHELL(_menu),l);
  gtk_widget_show_all(_menu);

  GtkWidget * vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
  gtk_container_set_resize_mode (GTK_CONTAINER(vbox), GTK_RESIZE_QUEUE);

  // about this expander... getting the ellipsis to work is a strange process.
  // once you turn ellipsize on, the expander tries to make its label as narrow
  // as it can and just have the three "..."s.  So gtk_label_set_width_chars
  // is used to force labels to want to be the right size... but then they
  // never ellipsize.  But, if we start with gtk_widget_set_size_request() to
  // tell the expander that it _wants_ to be very small, then it will still take
  // extra space given to it by its parent without asking for enough size to
  // fit the entire label.
  w = _expander = gtk_expander_new (nullptr);
  gtk_widget_set_size_request (w, 50, -1);
  g_signal_connect (w, "activate", G_CALLBACK(expander_activated_cb), this);
  gtk_box_pack_start (GTK_BOX(vbox), w, false, false, 0);
  gtk_box_pack_start (GTK_BOX(vbox), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), false, false, 0);

  _terse = gtk_label_new ("Expander");
  g_object_ref_sink (G_OBJECT(_terse));
  gtk_misc_set_alignment (GTK_MISC(_terse), 0.0f, 0.5f);
  gtk_label_set_use_markup (GTK_LABEL(_terse), true);
  gtk_label_set_selectable (GTK_LABEL(_terse), true);
  gtk_label_set_ellipsize (GTK_LABEL(_terse), PANGO_ELLIPSIZE_MIDDLE);
  gtk_widget_show (_terse);
  g_signal_connect (_terse, "button-press-event", G_CALLBACK(verbose_clicked_cb), this);

  hbox = _verbose = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
  g_object_ref_sink (G_OBJECT(_verbose));
  w = _headers = gtk_label_new ("Headers");
  gtk_label_set_selectable (GTK_LABEL(_headers), TRUE);
  gtk_misc_set_alignment (GTK_MISC(w), 0.0f, 0.5f);
  gtk_label_set_ellipsize (GTK_LABEL(w), PANGO_ELLIPSIZE_MIDDLE);
  gtk_label_set_use_markup (GTK_LABEL(w), true);
  gtk_box_pack_start (GTK_BOX(hbox), w, true, true, PAD_SMALL);

  w = _xface = gtk_image_new();
  gtk_widget_set_size_request (w, 0, FACE_SIZE);
  gtk_box_pack_start (GTK_BOX(hbox), w, false, false, PAD_SMALL);
  w = _face = gtk_image_new ();
  gtk_widget_set_size_request (w, 0, FACE_SIZE);
  gtk_box_pack_start (GTK_BOX(hbox), w, false, false, PAD_SMALL);
  gtk_widget_show_all (_verbose);
  g_signal_connect (_verbose, "button-press-event", G_CALLBACK(verbose_clicked_cb), this);
#ifdef HAVE_GMIME_CRYPTO
  gtk_widget_set_has_tooltip (_verbose, true);
  g_signal_connect(_verbose,"query-tooltip",G_CALLBACK(on_verbose_tooltip_cb), this);
#endif

  // setup
  _text = gtk_text_view_new ();
  refresh_fonts ();
  gtk_widget_add_events (_text, GDK_POINTER_MOTION_MASK|GDK_POINTER_MOTION_HINT_MASK);
  gtk_container_set_border_width (GTK_CONTAINER(_text), PAD_SMALL);
  gtk_text_view_set_editable (GTK_TEXT_VIEW(_text), false);
  gtk_text_view_set_cursor_visible (GTK_TEXT_VIEW(_text), false);
  _scroll = gtk_scrolled_window_new (nullptr, nullptr);
  gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (_scroll),
                                  GTK_POLICY_AUTOMATIC,
                                  GTK_POLICY_AUTOMATIC);
  gtk_container_add (GTK_CONTAINER(_scroll), _text);
  gtk_widget_show_all (vbox);
  gtk_box_pack_start (GTK_BOX(vbox), _scroll, true, true, 0);

  // add a toolbar for attachments
  _att_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
  GtkWidget * att_label = gtk_label_new (_("Attachments:"));
  gtk_misc_set_padding (GTK_MISC(att_label), PAD_SMALL, 0);
  gtk_misc_set_alignment (GTK_MISC(att_label), 0, 0);
  gtk_box_pack_start (GTK_BOX(_att_box), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), false, false, 0);
  gtk_box_pack_start (GTK_BOX(_att_box), att_label, false, false, 0);
  gtk_box_pack_start (GTK_BOX(vbox), create_attachments_toolbar (_att_box), false, false, 0);
  gtk_box_pack_start (GTK_BOX(vbox), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), false, false, 0);

  // set up the buffer tags
  _buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW(_text));
  set_text_buffer_tags (_buffer, _prefs);

  set_text_from_message (nullptr);
  const bool expanded (_prefs.get_flag ("body-pane-headers-expanded", true));
  gtk_expander_set_expanded (GTK_EXPANDER(_expander), expanded);
  expander_activated_idle (this);

#ifdef HAVE_WEBKIT
  w = gtk_notebook_new ();
  GtkNotebook * n (GTK_NOTEBOOK (w));
  gtk_notebook_append_page (n, vbox, gtk_label_new (_("Text View")));

  // add scroll to html
  GtkWidget* scroll = gtk_scrolled_window_new (nullptr, nullptr);
  gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW(scroll), GTK_SHADOW_IN);
  gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll),
                                  GTK_POLICY_AUTOMATIC,
                                  GTK_POLICY_AUTOMATIC);
  gtk_container_add (GTK_CONTAINER(scroll), _web_view);
  gtk_notebook_append_page (n, scroll, gtk_label_new (_("HTML View")));
#else
  w = vbox;
#endif
  _root = w;
  _prefs.add_listener (this);

  // listen for user interaction
  g_signal_connect (_text, "motion_notify_event", G_CALLBACK(motion_notify_event), &_hover_url);
  g_signal_connect (_text, "button_press_event", G_CALLBACK(mouse_button_pressed_cb), this);
  g_signal_connect (_text, "key_press_event", G_CALLBACK(text_key_pressed), _scroll);
  g_signal_connect (_text, "size_allocate", G_CALLBACK(text_size_allocated), this);
  g_signal_connect (_text, "populate_popup", G_CALLBACK(populate_popup_cb), this);
  g_signal_connect (_root, "show", G_CALLBACK(show_cb), this);

  gtk_widget_show_all (_root);
}

BodyPane :: ~BodyPane ()
{
  _prefs.remove_listener (this);

  g_object_unref (_verbose);
  g_object_unref (_terse);

  if (_message)
    g_object_unref (_message);

  for (int i=0; i<NUM_ICONS; ++i)
    g_object_unref (icons[i].pixbuf);

  foreach (std::set<char*>, _attach_names, it)
    g_free(*it);

  // store last opened message in prefs
  _prefs.set_string("last-opened-msg", get_cleared() ? "" : get_message_id().to_view());
  free_emoticons();
  free_cursors();
}


namespace
{
  const int smooth_scrolling_speed (10);

  void sylpheed_textview_smooth_scroll_do (GtkAdjustment  * vadj,
                                           gfloat           old_value,
                                           gfloat           new_value,
                                           int              step)
  {
    const bool down (old_value < new_value);
    const int change_value = (int)(down ? new_value-old_value : old_value-new_value);
    for (int i=step; i<=change_value; i+=step)
      gtk_adjustment_set_value (vadj, old_value+(down?i:-i));
    gtk_adjustment_set_value (vadj, new_value);
  }
}

bool
BodyPane :: read_more_or_less (bool more)
{
  GtkWidget * parent = gtk_widget_get_parent (_text);
  GtkAdjustment * v = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW(parent));

  // figure out how far we scroll
  const double v_value = gtk_adjustment_get_value(v);
  const double v_upper = gtk_adjustment_get_upper(v);
  const double v_lower = gtk_adjustment_get_lower(v);
  const double v_page_size = gtk_adjustment_get_page_size(v);
  const int arbitrary_font_height_pixels_hack (18);
  const float inc (v_page_size - arbitrary_font_height_pixels_hack);
  const gfloat val (CLAMP (v_value + (more ? inc : -inc),
                           v_lower,
                           MAX(v_upper,v_page_size)-MIN(v_upper,v_page_size)));

  // if we can scroll, do so.
  bool handled (false);
  if (v_upper>=v_page_size && val!=v_value)
  {
    if (_prefs.get_flag ("smooth-scrolling", true))
      sylpheed_textview_smooth_scroll_do (v, v_value, val, smooth_scrolling_speed);
    else
      gtk_adjustment_set_value (v, val);

    handled = true;
  }

  return handled;
}

namespace
{
  // (1) strip redundant leading Re: and RE:
  // (2) ensure the remaining Re: has a lowercase e

  std::string normalize_subject_re (const StringView& v_in)
  {
    StringView v(v_in), prev(v_in);
    while (!v.empty()) {
      v.ltrim ();
      StringView tmp (v);
      if (tmp.strstr("Re:") == tmp.str)
        tmp.eat_chars (3);
      else if (v.strstr("RE:") == tmp.str)
        tmp.eat_chars (3);
      else
        break;
      prev = v;
      v = tmp;
    }

    std::string ret (prev.str, prev.len);
    if (!ret.find("RE:")) // force lowercase 'e'
      ret.replace (0, 3, "Re:");

    return ret;
  }

  std::string get_header (GMimeMessage * msg,
                          const char   * key,
                          const char   * fallback_charset_1,
                          const char   * fallback_charset_2)
  {
    const StringView v (g_mime_object_get_header ((GMimeObject *) msg, key));
    std::string s;
    if (!v.empty())
      s = header_to_utf8 (v, fallback_charset_1, fallback_charset_2);
    return s;
  }

  struct ForeachPartData
  {
    std::string fallback_charset;
    std::string body;
  };

  void get_utf8_body_foreach_part (GMimeObject* /*parent*/, GMimeObject *o,
                                   gpointer user_data)
  {
    GMimePart * part;
    GMimeContentType * type = g_mime_object_get_content_type (o);
    const bool is_text (g_mime_content_type_is_type (type, "text", "*"));
    if (is_text)
    {
      part = GMIME_PART (o);
      ForeachPartData *data (static_cast<ForeachPartData*>(user_data));
      data->body += mime_part_to_utf8 (part, data->fallback_charset.c_str());
    }
  }

  std::string get_utf8_body (GMimeMessage * source,
                             const char   * fallback_charset)
  {
    ForeachPartData tmp;
    if (fallback_charset)
      tmp.fallback_charset = fallback_charset;
    if (source)
      g_mime_message_foreach (source, get_utf8_body_foreach_part, &tmp);
    return tmp.body;
  }
}

GMimeMessage*
BodyPane :: create_followup_or_reply (bool is_reply)
{
  GMimeMessage * msg (nullptr);

  if (_message)
  {
    msg = g_mime_message_new (false);
    GMimeObject *msg_obj = (GMimeObject*)msg;
    GMimeObject *_message_obj = (GMimeObject*)_message;

    // fallback character encodings
    const char * group_charset (_charset.c_str());
    GMimeContentType * type (g_mime_object_get_content_type (GMIME_OBJECT(_message)));
    const char * message_charset (type ? g_mime_content_type_get_parameter (type, "charset") : nullptr);

    ///
    ///  HEADERS
    ///

    // To:, Newsgroups:
    const std::string from       (get_header (_message, "From",        message_charset, group_charset));
    const std::string newsgroups (get_header (_message, "Newsgroups",  message_charset, group_charset));
    const std::string fup_to     (get_header (_message, "Followup-To", message_charset, group_charset));
    const std::string reply_to   (get_header (_message, "Reply-To",    message_charset, group_charset));

    if (is_reply || fup_to=="poster") {
      const std::string& to (reply_to.empty() ? from : reply_to);
      pan_g_mime_message_add_recipients_from_string (msg, GMIME_ADDRESS_TYPE_TO, to.c_str());
    } else {
      const std::string& groups (fup_to.empty() ? newsgroups : fup_to);
      g_mime_object_append_header ((GMimeObject *) msg, "Newsgroups", groups.c_str(), nullptr);
    }

    // Subject:
    StringView v = g_mime_object_get_header (_message_obj, "Subject");
    std::string h = header_to_utf8 (v, message_charset, group_charset);
    std::string val (normalize_subject_re (h));
    if (val.find ("Re:") != 0) // add "Re: " if we don't have one
      val.insert (0, "Re: ");
    g_mime_message_set_subject (msg, val.c_str(), nullptr);

    // attribution lines
    const char * cpch = g_mime_object_get_header (_message_obj, "From");
    h = header_to_utf8 (cpch, message_charset, group_charset);
    g_mime_object_append_header (msg_obj, "X-Draft-Attribution-Author", h.c_str(), nullptr);

    cpch = g_mime_message_get_message_id (_message);
    h = header_to_utf8 (cpch, message_charset, group_charset);
    g_mime_object_append_header (msg_obj, "X-Draft-Attribution-Id", h.c_str(), nullptr);

    const char * header_t = "Date";
    const char * tmp_s = g_mime_object_get_header (_message_obj, header_t);
    const char * tmp = tmp_s;  // FIXME: convert time to string
    h = header_to_utf8 (tmp, message_charset, group_charset);
    g_mime_object_append_header (msg_obj, "X-Draft-Attribution-Date", h.c_str(), nullptr);

    // references
    const char * header = "References";
    v = g_mime_object_get_header (_message_obj, header);
    val.assign (v.str, v.len);
    if (!val.empty())
      val += ' ';
    val += "<";
    val += g_mime_message_get_message_id (_message);
    val += ">";
    val = GNKSA :: trim_references (val);
    g_mime_object_append_header (msg_obj, header, val.c_str(), nullptr);

    ///
    ///  BODY
    ///

    GtkTextIter start, end;
    if (gtk_text_buffer_get_selection_bounds (_buffer, &start, &end))
    {
      // go with the user-selected region w/o modifications.
      // since it's in the text pane it's already utf-8...
      h = gtk_text_buffer_get_text (_buffer, &start, &end, false);
    }
    else
    {
      // get the body; remove the sig
      h = get_utf8_body (_message, group_charset);
      StringView v (h);
      int sig_index (0);
      if (GNKSA::find_signature_delimiter (h, sig_index) != GNKSA::SIG_NONE)
        v.len = sig_index;
      v.trim ();
      h = std::string (v.str, v.len);
    }

    // quote the body
    std::string s;
    for (const char *c(h.c_str()); c && *c; ++c) {
      if (c==h.c_str() || c[-1]=='\n')
        s += (*c=='>' ? ">" : "> ");
      s += *c;
    }

    // set the clone's content object with our modified body
    GMimeStream * stream = g_mime_stream_mem_new ();
    g_mime_stream_write_string (stream, s.c_str());
    GMimeDataWrapper * wrapper = g_mime_data_wrapper_new_with_stream (stream, GMIME_CONTENT_ENCODING_8BIT);
    GMimePart * part = g_mime_part_new ();
    GMimeContentType * new_type = g_mime_content_type_parse (nullptr, "text/plain; charset=UTF-8");
    g_mime_object_set_content_type ((GMimeObject *) part, new_type);
    g_mime_part_set_content (part, wrapper);
    g_mime_part_set_content_encoding (part, GMIME_CONTENT_ENCODING_8BIT);
    g_mime_message_set_mime_part (msg, GMIME_OBJECT(part));
    g_object_unref (new_type);
    g_object_unref (wrapper);
    g_object_unref (part);
    g_object_unref (stream);

  //std::cerr << LINE_ID << " here is the modified clone\n [" << g_mime_object_to_string((GMimeObject *)msg) << ']' << std::endl;

  }

  return msg;
}

/***
****
***/

void
BodyPane :: refresh_fonts ()
{
  const bool body_pane_font_enabled = _prefs.get_flag ("body-pane-font-enabled", false);
  const bool monospace_font_enabled = _prefs.get_flag ("monospace-font-enabled", false);

  if (!body_pane_font_enabled && !monospace_font_enabled)
    gtk_widget_override_font (_text, nullptr);
  else {
    const std::string str (monospace_font_enabled
      ? _prefs.get_string ("monospace-font", "Monospace 10")
      : _prefs.get_string ("body-pane-font", "Sans 10"));
    PangoFontDescription * pfd (pango_font_description_from_string (str.c_str()));
    gtk_widget_override_font (_text, pfd);
    pango_font_description_free (pfd);
  }
}

void
BodyPane :: on_prefs_flag_changed (const StringView& key, bool value G_GNUC_UNUSED)
{
  if ((key=="body-pane-font-enabled") || (key=="monospace-font-enabled"))
    refresh_fonts ();

  if ((key=="wrap-article-body") || (key=="mute-quoted-text") ||
      (key=="mute-signature") ||
      (key=="show-smilies-as-graphics") || (key=="show-all-headers") ||
      (key=="size-pictures-to-fit") || (key=="show-text-markup") ||
      (key=="highlight-urls") ||
      (key=="show-article-sig")
     )
    refresh ();
}

void
BodyPane :: on_prefs_string_changed (const StringView& key, const StringView& value G_GNUC_UNUSED)
{
  if ((key=="body-pane-font") || (key=="monospace-font"))
    refresh_fonts ();

}

void
BodyPane :: on_prefs_color_changed (const StringView& key, const GdkRGBA& color G_GNUC_UNUSED)
{
  if ((key == "text-color-fg")              || (key == "text-color-bg")              ||
      (key == "body-pane-color-url")        || (key == "body-pane-color-url-bg")     ||
      (key == "body-pane-color-quote-1")    || (key == "body-pane-color-quote-2")    ||
      (key == "body-pane-color-quote-3")    || (key == "body-pane-color-quote-1-bg") ||
      (key == "body-pane-color-quote-2-bg") || (key == "body-pane-color-quote-3-bg") ||
      (key == "body-pane-color-signature")  || (key == "body-pane-color-signature-bg"))
  {
    refresh_colors ();
  }
}

void
BodyPane :: refresh_colors ()
{
  set_text_buffer_tags (_buffer, _prefs);
  set_text_from_message (_message);
}

void
BodyPane :: set_character_encoding (const char * charset)
{
  if (charset && *charset)
    _charset = charset;
  else
    _charset.clear ();

  refresh ();
}

}
