Logo Search packages:      
Sourcecode: pan version File versions  Download package

article.c

/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
 * Pan - A Newsreader for Gtk+
 * Copyright (C) 2002  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, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include <config.h>

#include <ctype.h>
#include <stdlib.h>
#include <string.h>

#include <glib.h>

#include <pan/base/acache.h>
#include <pan/base/article.h>
#include <pan/base/article-thread.h>
#include <pan/base/debug.h>
#include <pan/base/gnksa.h>
#include <pan/base/group.h>
#include <pan/base/pan-glib-extensions.h>
#include <pan/base/pan-i18n.h>
#include <pan/base/server.h>
#include <pan/base/group.h>
#include <pan/base/util-mime.h>

const char * default_incoming_name_addr;
const char * default_incoming_name_real;

static void fire_articles_changed (Group*, Article**, int article_qty, ArticleChangeType);

/***
****
****  LIFE CYCLE
****
***/

#define DEAD_POINTER ((void*)(0xDEADBEEF))

void
article_destructor (Article * a)
{
      static Article dead_article;
      static gboolean dead_article_inited = FALSE;

      /* sanity clause */
        g_return_if_fail (a != NULL);

      /* make sure dead_article is initialized */
      if (!dead_article_inited)
      {
            dead_article_inited = TRUE;

            dead_article.is_new           = ~0;
            dead_article.passes_filter    = ~0;
            dead_article.error_flag       = ~0;
            dead_article.multipart_state  = ~0;
            dead_article.decode_state     = ~0;
            dead_article.part             = ~0;
            dead_article.parts            = ~0;
            dead_article.score            = ~0;
            dead_article.score_date       = ~0;
            dead_article.byte_qty         = ~0;
            dead_article.linecount        = ~0;
            dead_article.unread_children  = ~0;
            dead_article.date             = ~0;
            dead_article.threads          = DEAD_POINTER;
            dead_article.parent           = DEAD_POINTER;

            dead_article.author_addr      = PSTRING_INIT;
            dead_article.author_real      = PSTRING_INIT;
            dead_article.references       = PSTRING_INIT;
            dead_article.xref             = PSTRING_INIT;
            dead_article.subject          = PSTRING_INIT;
            dead_article.message_id       = PSTRING_INIT;
      }

      /* destroy the article */
      pstring_clear (&a->author_addr);
      pstring_clear (&a->author_real);
      pstring_clear (&a->references);
      pstring_clear (&a->xref);
      pstring_clear (&a->subject);
      pstring_clear (&a->message_id);
      g_slist_free (a->threads);
      *a = dead_article;
}

Article*
article_new (Group * group)
{
      Article * article;
      static Article empty_article;
      static gboolean empty_article_inited = FALSE;

      /* sanity clause */
      g_return_val_if_fail (group!=NULL, NULL);

      /* make sure the zero struct is initialized */
      if (!empty_article_inited)
      {
            empty_article_inited = TRUE;

            empty_article.is_new           = FALSE;
            empty_article.passes_filter    = FALSE;
            empty_article.error_flag       = FALSE;
            empty_article.multipart_state  = (guint8)0;
            empty_article.decode_state     = (guint8)0;
            empty_article.part             = (gint16)0;
            empty_article.parts            = (gint16)0;
            empty_article.score            = (gint16)0;
            empty_article.byte_qty         = (gulong)0;
            empty_article.linecount        = (guint16)0;
            empty_article.unread_children  = (guint16)0;
            empty_article.new_children     = (guint16)0;
            empty_article.score_date       = (time_t)0;
            empty_article.date             = (time_t)0;
            empty_article.number           = (gulong)0;
            empty_article.parent           = NULL;
            empty_article.threads          = NULL;
            empty_article.group            = DEAD_POINTER;

            empty_article.author_addr      = PSTRING_INIT;
            empty_article.author_real      = PSTRING_INIT;
            empty_article.references       = PSTRING_INIT;
            empty_article.xref             = PSTRING_INIT;
            empty_article.subject          = PSTRING_INIT;
            empty_article.message_id       = PSTRING_INIT;
      }

      /* create the new article */
      article = group_alloc_new_article (group);
      *article = empty_article;
      article->group = group;

      return article;
}

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

void
articles_set_dirty (Article ** articles, int qty)
{
      Group * group;
      debug_enter ("articles_set_dirty");

      /* sanity check */
      g_return_if_fail (qty >= 1);
      g_return_if_fail (articles_are_valid_in_group ((const Article **)articles, qty));

      /* mark the articles' group as dirty */
      group = articles[0]->group;
      group_set_articles_dirty (group);
      fire_articles_changed (group, articles, qty, ARTICLE_CHANGED_DIRTY);

      debug_exit ("articles_set_dirty");
}


gboolean
article_is_new (const Article * a)
{
      g_return_val_if_fail (article_is_valid(a), FALSE);

      return a->is_new && !article_is_read(a);
}

/***
****  NEW / OLD
***/

static void
articles_set_new_impl (Article ** articles, int article_qty, gboolean is_new, gboolean fire)
{
      int i;
      GPtrArray * changed;
      debug_enter ("articles_set_new_impl");

      /* sanity check */
      g_return_if_fail (article_qty > 0);
      g_return_if_fail (articles_are_valid_in_group ((const Article **)articles, article_qty));

      /* mark each article's newness field */
      changed = g_ptr_array_sized_new (article_qty);
      for (i=0; i<article_qty; ++i) {
            Article * a = articles[i];
            if (!!is_new != !!a->is_new) {
                  a->is_new = is_new;
                  g_ptr_array_add (changed, a);
            }
      }

      if (changed->len)
      {
            /* update parent counts of changed articles */
            for (i=0; i<changed->len; ++i) {
                  Article * a = ARTICLE(g_ptr_array_index(changed, i));
                  for (a=a->parent; a!=NULL; a=a->parent)
                        a->new_children += is_new ? 1 : -1;
            }

            /* fire notification of changed articles */
            if (fire)
                  fire_articles_changed (articles[0]->group, (Article**)changed->pdata, changed->len, ARTICLE_CHANGED_NEW);
      }

      /* cleanup */
      g_ptr_array_free (changed, TRUE);
      debug_exit ("articles_set_new_impl");
}

void
articles_set_new (Article ** articles, int article_qty, gboolean is_new)
{
      articles_set_new_impl (articles, article_qty, is_new, TRUE);
}

/***
****
****  READ / UNREAD
****
***/

gboolean
article_is_read (const Article * a)
{
      g_return_val_if_fail (a!=NULL, FALSE);
      g_return_val_if_fail (a->group!=NULL, FALSE);

      return group_is_article_read (a->group, a->number);
}

void
articles_set_read_simple (Article    ** articles,
                          int           article_qty,
                          gboolean      read)
{
      guint i;
      Group * g;
      Article ** changed;
      int changed_qty;

      /* sanity clause */
      g_return_if_fail (article_qty>0);
      g_return_if_fail (articles_are_valid_in_group ((const Article **)articles, article_qty));

      /* prep local variables */
      g = articles[0]->group;
      changed = g_new (Article*, article_qty);
      changed_qty = 0;

      /* find the articles that will be changed by this action */
      for (i=0; i<article_qty; ++i)
            if (!!read != !!group_mark_article_read (g, articles[i]->number, read))
                  changed[changed_qty++] = articles[i];

      /**
      ***  If anything is to be changed:
      ***  (1) update parent counts
      ***  (2) update group's counts
      ***  (3) fire an event
      **/

      if (changed_qty != 0)
      {
            /* update parent counts */
            for (i=0; i!=changed_qty; ++i)
            {
                  Article * a = changed[i];
                  for (a=a->parent; a!=NULL; a=a->parent)
                        a->unread_children += read ? -1 : 1;
            }
            if (read)
                  articles_set_new_impl (changed, changed_qty, FALSE, FALSE);

            /* update the group's count */
            group_inc_article_read_qty (g, read ? changed_qty : -changed_qty);

            /* fire an event */
            fire_articles_changed (g, changed, changed_qty, ARTICLE_CHANGED_READ);
      }

      /* cleanup */
      g_free (changed);
}

static void
articles_set_read_numbers_ghfunc (gpointer key, gpointer value, gpointer user_data)
{
      int i;
      int changed_qty;
      Group * g = GROUP(key);
      GArray * numbers = (GArray*) value;
      const gboolean read = user_data!=NULL;

      /* Mark each number as read/unread */
      for (i=changed_qty=0; i<numbers->len; ++i) {
            const gulong number = g_array_index (numbers, gulong, i);
            if (read != group_mark_article_read (g, number, read))
                  ++changed_qty;
      }

      /* If any changed, update the group's count */
      if (changed_qty != 0)
            group_inc_article_read_qty (g, read ? changed_qty : -changed_qty);

      /* cleanup */
      g_array_free (numbers, TRUE);
}

/**
 * Builds a GHashTable of Group* -> GArray*, where the GArray is filled with message numbers.
 */
static void
mark_read_xreffunc (Server * server, Group * g, gulong number, gpointer user_data)
{
      GHashTable ** group_to_numbers = (GHashTable**) user_data;

      if (group_is_subscribed(g))
      {
            GArray * group_numbers;

            /* make sure *group_to_numbers holds a hash table */
            if (*group_to_numbers == NULL)
                  *group_to_numbers = g_hash_table_new (g_direct_hash, g_direct_equal);

            /* make sure group_numbers is a GArray of ulongs in *group_to_numbers */
            group_numbers = (GArray*) g_hash_table_lookup (*group_to_numbers, g);
            if (group_numbers == NULL) {
                  group_numbers = g_array_new (FALSE, FALSE, sizeof(gulong));
                  g_hash_table_insert (*group_to_numbers, g, group_numbers);
            }

            g_array_append_val (group_numbers, number);
      }
}

static void
articles_set_read_and_crossposts (Article ** articles, int article_qty, gboolean read)
{
      int i;
      GHashTable * group_to_numbers = NULL;
      debug_enter ("articles_set_read_and_crossposts");

      /* sanity check */
      g_return_if_fail (article_qty > 0);
      g_return_if_fail (articles != NULL);
      g_return_if_fail (articles_are_valid_in_group ((const Article **)articles, article_qty));

      /* mark the Articles as read */
      articles_set_read_simple (articles, article_qty, read);

      /* collect the crossposts.
       * this is done with a hash of Group* key to an array of gulong article indices
       * from the article's xref header. */
      for (i=0; i<article_qty; ++i)
            article_xref_foreach (articles[i],
                                  mark_read_xreffunc,
                                  &group_to_numbers,
                                  SERVER_GROUPS_SUBSCRIBED,
                                  TRUE);

      /* now that we've organized the crossposts by group,
       * mark them read with one call per group
       * to minimize the number of events fired. */
      if (group_to_numbers != NULL) {
            g_hash_table_foreach (group_to_numbers,
                                  articles_set_read_numbers_ghfunc,
                                  GINT_TO_POINTER(read));
            g_hash_table_destroy (group_to_numbers);
      }

      debug_exit ("articles_set_read_and_crossposts");
}

void
articles_set_read (Article ** articles, int article_qty, gboolean read)
{
      int i;
      GPtrArray * all_articles;
      debug_enter ("articles_set_read");

      /* sanity check */
      g_return_if_fail (article_qty > 0);
      g_return_if_fail (articles_are_valid_in_group ((const Article **)articles, article_qty));

      /* add all the articles passed in */
      all_articles = g_ptr_array_new ();
      pan_g_ptr_array_assign (all_articles, (gpointer*)articles, article_qty);

      /* multiparts are marked read/unread as a set,
       * so if any of the articles passed in are multiparts,
       * add the children to the list too */
      for (i=0; i<article_qty; ++i) {
            const Article * a = articles[i];
            if (a->part==1 && a->parts>1 && a->threads!=NULL) {
                  GSList * l;
                  for (l=a->threads; l!=NULL; l=l->next) {
                        Article * child = ARTICLE(l->data);
                        if (child->part>a->part && child->parts==a->parts)
                              g_ptr_array_add (all_articles, child);
                  }
            }
      }

      /* mark them as read/unread */
      articles_set_read_and_crossposts ((Article**)all_articles->pdata, all_articles->len, read);

      /* cleanup */
      g_ptr_array_free (all_articles, TRUE);
      debug_exit ("articles_set_read");
}



/**
*** PUBLIC MUTATORS
**/

int
article_get_multipart_state (const Article * article)
{
      g_return_val_if_fail (article_is_valid (article), 0);

      return article->multipart_state;
}

void
articles_set_multipart_state (Article  ** articles,
                              int         qty,
                              int         state)
{
      int i;
      int changed_qty;
      Article ** changed;

      /* sanity clause */
      g_return_if_fail (qty >= 1);
      g_return_if_fail (articles_are_valid_in_group ((const Article **)articles, qty));
      g_return_if_fail (state==MULTIPART_STATE_NONE || state==MULTIPART_STATE_SOME || state==MULTIPART_STATE_ALL);

      /* mark 'em */
      changed = g_newa (Article*, qty);
      for (changed_qty=i=0; i<qty; ++i) {
            Article * a = articles[i];
            guint8 * pstate = &a->multipart_state;
            if (*pstate != state) {
                  *pstate = state;
                  changed[changed_qty++] = a;
            }
            if (articles[i]->multipart_state != state) {
                  articles[i]->multipart_state = state;
                  changed[changed_qty++] = articles[i];
            }
      }

      if (changed_qty > 0)
            articles_set_dirty (changed, changed_qty);
}

int
article_get_decode_state (const Article * article)
{
      g_return_val_if_fail (article_is_valid (article), 0);

      return article->decode_state;
}

void
articles_set_decode_state (Article  ** articles,
                           int         qty,
                           int         state)
{
      int i;
      int changed_qty;
      Article ** changed;

      /* sanity clause */
      g_return_if_fail (qty >= 1);
      g_return_if_fail (articles_are_valid_in_group ((const Article **)articles, qty));
      g_return_if_fail (state==DECODE_STATE_NONE || state==DECODE_STATE_DECODED || state==DECODE_STATE_FAILED);

      /* mark 'em */
      changed = g_newa (Article*, qty);
      for (changed_qty=i=0; i<qty; ++i) {
            Article * a = articles[i];
            guint8 * pstate = &a->decode_state;
            if (*pstate != state) {
                  *pstate = state;
                  changed[changed_qty++] = a;
            }
      }

      if (changed_qty > 0)
            articles_set_dirty (changed, changed_qty);
}


void
article_set_error_flag (Article * article, gboolean error_flag)
{
      g_return_if_fail (article_is_valid (article));

      if (article->error_flag != error_flag)
      {
            article->error_flag = error_flag;

            articles_set_dirty (&article, 1);
      }
}



/***
****
****  THREADS
****
***/

Article*
article_get_root (const Article* article)
{
      g_return_val_if_fail (article_is_valid(article), (Article*)article);

      while (article->parent != NULL)
            article = article->parent;

      return (Article*) article;
}

gboolean
articles_are_in_same_thread (const Article  * article_1,
                             const Article  * article_2)
{
      if ((article_1!=NULL) != (article_2!=NULL))
            return FALSE;

      return article_get_root(article_1) == article_get_root(article_2);
}

char*
article_get_thread_message_id (const Article * article)
{
      char * pch;
      const char * refs;

      /* sanity clause */
      g_return_val_if_fail (article_is_valid(article), NULL);

      /* go up as high as we can from our own threading, because sometimes
       * the References: header is broken by other newreaders. */
      article = article_get_root (article);

      refs = article->references.str;
      if (is_nonempty_string(refs))
      {
            pch = get_next_token_str (refs, ' ', NULL);
      }
      else /* top of the thread */
      {
            pch = g_strdup (article_get_message_id (article));
      }

      return pch;
}

static void
article_get_thread_impl (const Article * top, GPtrArray * setme)
{
      GSList *l;
      g_ptr_array_add (setme, (gpointer)top);
      for (l=top->threads; l!=NULL; l=l->next)
            article_get_thread_impl (ARTICLE(l->data), setme);
}


void
article_forall_in_subthread   (Article            * article,
                               ArticleForall        forall_func,
                               gpointer             forall_func_user_data)
{
      GPtrArray * a;

      /* sanity check */
      g_return_if_fail (article_is_valid (article));

      /* forall */
      a = g_ptr_array_sized_new (128);
      article_get_thread_impl (article, a);
      if (a->len)
            (forall_func)((Article**)a->pdata, a->len, forall_func_user_data);
      g_ptr_array_free (a, TRUE);
}

void
article_forall_in_thread    (Article           * article,
                             ArticleForall       forall_func,
                             gpointer            forall_user_data)
{
      article_forall_in_subthread (article_get_root(article), forall_func, forall_user_data);
}

void
article_forall_in_references  (Article            * article,
                               ArticleForall        forall_func,
                               gpointer             forall_func_user_data)
{
      GPtrArray * a;

      g_return_if_fail (article_is_valid(article));

      a = g_ptr_array_sized_new (128);
      while (article != NULL) {
            g_ptr_array_add (a, (gpointer)article);
            article = article->parent;
      }

      if (a->len)
            (forall_func)((Article**)a->pdata, a->len, forall_func_user_data);

      g_ptr_array_free (a, TRUE);
}


static void
add_articles_to_direct_hash (Article        ** articles,
                             guint             article_qty,
                             gpointer          article_hash_gpointer)
{
      guint i;
      GHashTable * article_hash = (GHashTable*) article_hash_gpointer;
      for (i=0; i!=article_qty; ++i)
            g_hash_table_insert (article_hash, articles[i], articles[i]);
}


void
article_forall_in_threads (Article           ** articles,
                           guint                article_qty,
                           ThreadGet            thread_get,
                           ArticleForall        forall_func,
                           gpointer             forall_user_data)
{
      guint i;
      GHashTable * hash;
      GPtrArray * array;
      debug_enter ("article_forall_in_threads");

      /* get the thread for each article passed in */
      hash = g_hash_table_new (g_direct_hash, g_direct_equal);
      for (i=0; i!=article_qty; ++i)
      {
            Article * a = articles[i];

            /* if we already have the article, then we have its thread, so skip */
            if (g_hash_table_lookup (hash, a) != NULL)
                  continue;

            /* get the thread associated with sel... */
            if (thread_get == GET_SUBTHREAD)
                  article_forall_in_subthread (a, add_articles_to_direct_hash, hash);
            else
                  article_forall_in_thread (a, add_articles_to_direct_hash, hash);
      }

      /* forall */
      array = g_ptr_array_new ();
      pan_hash_to_ptr_array (hash, array);
      if (array->len != 0u)
            (forall_func)((Article**)array->pdata, array->len, forall_user_data);

      /* cleanup */
      g_ptr_array_free (array, TRUE);
      g_hash_table_destroy (hash);
      debug_enter ("article_forall_in_threads");
}

/***
****  XREF
***/

void
xref_header_foreach (const char                * xref_string,
                     const PString             * server_name,
                     const PString             * skip_group,
                     ArticleXRefStringFunc       func,
                     gpointer                    user_data)
{
      PString run;
      const char * march = NULL;

      /* skip the servername; we've got the server already */
      skip_next_token (xref_string, ' ', &march);

      /* walk through the xrefs, of format "group1:number group2:number" */
      while (get_next_token_pstring (march, ' ', &march, &run))
      {
            const char * delimit = pan_strchr_len (run.str, run.len, ':');
            if (delimit != NULL)
            {
                  const gulong number = strtoul (delimit+1, NULL, 10);
                  const PString group_name = pstring_substr_shallow (&run, NULL, delimit);
                  if (skip_group==NULL || !pstring_equal (&group_name, skip_group))
                        (*func)(server_name, &group_name, number, user_data);
            }
      }
}

typedef struct
{
      Server * server;
      ArticleXRefFunc * func;
      gpointer user_data;
      ServerGroupsType set;
}
XRefForeachData;

static void
article_xref_foreach_impl (const PString             * server_name,
                           const PString             * group_name,
                           gulong                      number,
                           gpointer                    user_data)
{
      XRefForeachData * data = (XRefForeachData*) user_data;
      Group * group = server_get_named_group_in_type (data->server, group_name, data->set);
      if (group!=NULL)
            (data->func)(data->server, group, number, data->user_data);
}

void
article_xref_foreach (const Article       * a,
                      ArticleXRefFunc       func,
                      gpointer              user_data,
                  ServerGroupsType      set,
                      gboolean              skip_group_a)
{
      XRefForeachData data;

      g_return_if_fail (article_is_valid(a));

      data.server = a->group->server;
      data.func = func;
      data.user_data = user_data;
      data.set = set;

      xref_header_foreach (a->xref.str,
                           &a->group->server->name,
                           skip_group_a ? &a->group->name : NULL,
                           article_xref_foreach_impl, &data);
}

/***
****
****  OTHER HEADERS
****
***/

int
article_get_crosspost_qty (const Article  * article)
{
      int retval;

      g_return_val_if_fail (article_is_valid (article), 0);

      if (!pstring_is_set (&article->xref))
            retval = 1;
      else {
            const char * pch;
            const char * const end = article->xref.str + article->xref.len;
            retval = 0;
            for (pch=article->xref.str; pch!=end; ++pch)
                  if (*pch==':')
                        ++retval;
      }

      return retval;
}

gboolean
article_header_is_internal (const char * key)
{
      return is_nonempty_string(key) && !strncmp(key,"X-Pan-Internal-",15);
}

gboolean
article_header_is_extra (const char * key)
{
      /* sanity check */
      if (!is_nonempty_string(key)) return FALSE;

      /* pan internals aren't user-specified headers */
      if (article_header_is_internal(key)) return FALSE;

      /* other headers that are handled explicitly elsewhere */
      if (!strcmp(key,HEADER_FOLLOWUP_TO)) return FALSE;
      if (!strcmp(key,HEADER_NEWSGROUPS)) return FALSE;
      if (!strcmp(key,HEADER_ORGANIZATION)) return FALSE;
      if (!strcmp(key,HEADER_REPLY_TO)) return FALSE;
      if (!strcmp(key,HEADER_DATE)) return FALSE;
      if (!strcmp(key,HEADER_XREF)) return FALSE;
      if (!strcmp(key,HEADER_FROM)) return FALSE;

      return TRUE;
}

void
article_set_xref (Article        * a,
                const PString  * xref)
{
      g_return_if_fail (a!=NULL);
      g_return_if_fail (pstring_is_valid (xref));

      pstring_copy (&a->xref, xref);
}

void
article_set_subject (Article        * a,
                   const PString   * subject)
{
      g_return_if_fail (a!=NULL);
      g_return_if_fail (pstring_is_valid (subject));

      pstring_copy (&a->subject, subject);
}
 
const char*
article_get_subject (const Article * a)
{
      g_return_val_if_fail (article_is_valid(a), "");

      return pstring_is_set (&a->subject) ? a->subject.str : "";
}


static double
string_likeness (const PString * str1, const PString * str2)
{
      double retval;

      if (pstring_strchr (str1, ' ') == NULL) /* only one word, so count common characters */
      {
            int i;
            int common_chars = 0;

            for (i=0; i<str1->len; ++i)
                  if (pstring_strchr (str2, str1->str[i]) != NULL)
                        ++common_chars;

            retval = (double)common_chars / str1->len;
      }
      else /* more than one word, so count common words */
      {
            int str1_words = 0;
            int common_words = 0;
            PString march = *str1;
            PString token;

            /* if the word is also in str2, increment common_words */
            while (pstring_next_token_shallow (&march, ' ', &march, &token)) {
                  if (token.len) {
                        const char * pch = pstring_strstr (str2, &token);
                        if (pch!=NULL
                            && (pch==str2->str || pch[-1]==' ')
                            && (pch+token.len==str2->str+str2->len || pch[token.len]==' '))
                              ++common_words;
                        ++str1_words;

                  }
            }

            retval = (double)common_words / str1_words;
      }

      return retval;
}

gboolean
article_subjects_are_similar (const Article  * article_1,
                              const Article  * article_2)
{
      int i;
      double min_closeness;
      PString subj_1 = PSTRING_INIT;
      PString subj_2 = PSTRING_INIT;
      static const char * const common_substrings [] = { "mp3", "gif", "jpg", "jpeg", "yEnc" };

      /* make our own copies of the strings so that we can mutilate them */
      pstring_copy_a (&subj_1, &article_1->subject);
      pstring_copy_a (&subj_2, &article_2->subject);

      /* white-out the common substrings that most often skew string_likeness too high */
      for (i=0; i!=G_N_ELEMENTS(common_substrings); ++i)
      {
            const char * needle = common_substrings[i];
            const int needle_len = strlen (needle);
            char * pch;

            if ((pch = pan_strnstr_len (subj_1.str, subj_1.len, needle, needle_len)))
                  memset (pch, 0, needle_len);
            if ((pch = pan_strnstr_len (subj_2.str, subj_2.len, needle, needle_len)))
                  memset (pch, 0, needle_len);
      }

      /* white-out non-alpha characters */
      {
            char * march;
            char * end;

            for (march=subj_1.str, end=subj_1.str+subj_1.len; march!=end; ++march)
                  if (!isalpha(*march))
                        *march = ' ';
            for (march=subj_2.str, end=subj_2.str+subj_2.len; march!=end; ++march)
                  if (!isalpha(*march))
                        *march = ' ';
      }

      /* decide how picky we want to be.
         - The shorter the string, the more alike they have to be.
         - longer strings typically include long unique filenames. */
      {
            const int min_len = MIN (article_1->subject.len, article_2->subject.len);
            const gboolean is_short_string = min_len <= 20;
            const gboolean is_long_string = min_len >= 30;

            if (is_short_string)
                  min_closeness = 0.6;
            else if (is_long_string)
                  min_closeness = 0.5;
            else
                  min_closeness = 0.55;
      }

      return string_likeness (&subj_1, &subj_2)  >= min_closeness;
}


void
article_set_message_id (Article        * a,
                      const PString  * message_id)
{
      g_return_if_fail (a!=NULL);
      g_return_if_fail (pstring_is_set (message_id));

      pstring_copy (&a->message_id, message_id);
}

const char*
article_get_message_id (const Article * a)
{
      g_return_val_if_fail (article_is_valid(a), "");

      return pstring_is_set (&a->message_id) ? a->message_id.str : "";
}

void
article_set_references (Article        * a,
                      const PString  * references)
{
      g_return_if_fail (a!=NULL);
      g_return_if_fail (pstring_is_set (references));

      pstring_copy (&a->references, references);
}

char*
article_format_author_str (const PString * author_addr,
                           const PString * author_real,
                           char          * buf,
                           int             bufsize)
{
      *buf = '\0';

      if (author_real->len && author_addr->len)
      {
            if (author_real->len + author_addr->len + 6 > bufsize)
            {
                  g_snprintf (buf, bufsize, "\"%*.*s\" <%*.*s>",
                              author_real->len, author_real->len, author_real->str,
                              author_addr->len, author_addr->len, author_addr->str);
            }
            else
            {
                  /* doing this by hand because profiling says it's a hotspot */
                  char * pch = buf;
                  *pch++ = '"';
                  memcpy (pch, author_real->str, author_real->len);
                  pch += author_real->len;
                  *pch++ = '"';
                  *pch++ = ' ';
                  *pch++ = '<';
                  memcpy (pch, author_addr->str, author_addr->len);
                  pch += author_addr->len;
                  *pch++ = '>';
                  *pch++ = '\0';
            }
      }
      else if (author_addr->len)
            pan_strncpy_len (buf, bufsize, author_addr->str, author_addr->len);
      else if (author_real->len)
            pan_strncpy_len (buf, bufsize, author_real->str, author_real->len);

      g_strstrip (buf);
      return buf;
}

char*
article_get_author_str (const Article * a, char * buf, int bufsize)
{
      /* sanity clause */
      g_return_val_if_fail (buf!=NULL, buf);
      g_return_val_if_fail (bufsize>0, buf);
      *buf = '\0';
      g_return_val_if_fail (article_is_valid(a), buf);

      return article_format_author_str (&a->author_addr,
                                        &a->author_real,
                                        buf, bufsize);
}

int
article_format_short_author_str (const PString * author_addr,
                                 const PString * author_real,
                                 char          * buf,
                                 int             buflen)
{
      int retval;
      static PString paddr;
      static PString preal;
      static gboolean init = FALSE;

      /* sanity clause */
      g_return_val_if_fail (buf!=NULL, 0);
      g_return_val_if_fail (buflen>0, 0);

      /* setup */
      if (!init) {
            paddr = pstring_shallow (default_incoming_name_addr, -1);
            preal = pstring_shallow (default_incoming_name_real, -1);
            init = TRUE;
      }

      if (pstring_is_set(author_real) && !pstring_equal(author_real, &preal))
      {
            retval = pan_strncpy_len (buf, buflen, author_real->str, author_real->len);
      }
      else if (pstring_is_set(author_addr) && !pstring_equal (author_addr, &paddr))
      {
            const char * cpch = pan_strchr_len (author_addr->str, author_addr->len, '@');
            const PString name = pstring_substr_shallow (author_addr, author_addr->str, cpch);
            retval = pan_strncpy_len (buf, buflen, name.str, name.len);
      }
      else
      {
            retval = pan_strncpy_len (buf, buflen, preal.str, preal.len);
      }

      return retval;
}

int
article_get_short_author_str (const Article * a, char * buf, int len)
{
      return article_format_short_author_str (&a->author_addr,
                                              &a->author_real,
                                              buf, len);
}

void
article_set_author (Article       * a,
                    const PString * header_from)
{
      char addr_buf[512];
      char real_buf[512];
      char * addr = addr_buf;
      char * real = real_buf;
      char * pch;
      const char * charset;

      /* sanity clause */
      g_return_if_fail (a!=NULL);
      g_return_if_fail (a->group!=NULL);
      g_return_if_fail (pstring_is_set (header_from));

      /* get the author address & real name */
      charset = group_get_default_charset (a->group);
      pch = pan_header_to_utf8 (header_from->str, header_from->len, charset);
      gnksa_do_check_from (pch,
                           addr_buf, sizeof(addr_buf),
                           real_buf, sizeof(real_buf),
                           FALSE); /* not strict */
      g_free (pch);

      /* use the real mail address, or fill in a default */
      pch = addr;
      if (!is_nonempty_string (pch))
            pch = (char*)default_incoming_name_addr;
      pstring_set (&a->author_addr, pch, -1);

      /* use the real name, or fill in a default. */
      if (is_nonempty_string(real)) {
            gnksa_strip_realname (real);
            pstring_set (&a->author_real, real, -1);
      }
      else if (addr!=NULL && ((pch=strchr(addr,'@'))!=NULL))
            pstring_set (&a->author_real, addr, pch-addr);
      else
            pstring_set (&a->author_real, default_incoming_name_real, -1);
}

void
article_set_from_g_mime_message   (Article         * a,
                                   GMimeMessage    * msg)
{
      const char * cpch;
      debug_enter ("article_set_from_g_mime_message");

      /* get the message */
      g_return_if_fail (a != NULL);
      g_return_if_fail (GMIME_IS_MESSAGE(msg));

      g_mime_message_get_date (msg, &a->date, NULL);

      cpch = g_mime_message_get_sender (msg);
      if (cpch != NULL) {
            const PString author = pstring_shallow (cpch, -1);
            article_set_author (a, &author);
      }

      cpch = g_mime_message_get_subject (msg);
      if (cpch != NULL) {
            const PString subject = pstring_shallow (cpch, -1);
            article_set_subject (a, &subject);
      }

      cpch = g_mime_message_get_message_id (msg);
      if (cpch != NULL) {
            const PString message_id = pstring_shallow (cpch, -1);
            article_set_message_id (a, &message_id);
      }

      cpch = g_mime_message_get_header (msg, HEADER_REFERENCES);
      if (cpch != NULL) {
            const PString references = pstring_shallow (cpch, -1);
            article_set_references (a, &references);
      }
      
      /* fallback if no message-id */
      if (!pstring_is_set (&a->message_id)) {
            PString id = pstring_shallow (gnksa_generate_message_id_from_email_addr (a->author_addr.str), -1);
            const PString default_subject = pstring_shallow (_("Unparseable Subject"), -1);
            g_warning (_("Couldn't parse a Message-ID from the raw message!"));
            article_set_message_id (a, &id);
            article_set_subject (a, &default_subject);
            pstring_clear (&id);
      }


      /* cleanup */
      debug_exit ("article_set_from_g_mime_message");
}

/****
*****  SANITY CHECKS
****/

gboolean
article_is_valid (const Article * a)
{
      g_return_val_if_fail (a!=NULL, FALSE);

      g_return_val_if_fail (a->group!=NULL, FALSE);
      g_return_val_if_fail (a->group != DEAD_POINTER, FALSE);
      g_return_val_if_fail (group_is_valid(a->group), FALSE);
      g_return_val_if_fail (a->group->_articles_refcount>=0, FALSE);
      g_return_val_if_fail (group_is_folder(a->group) || a->group->_articles_refcount>0, FALSE);

      g_return_val_if_fail (pstring_is_valid (&a->xref), FALSE);

      g_return_val_if_fail (pstring_is_set (&a->subject), FALSE);

      g_return_val_if_fail (pstring_is_set (&a->message_id), FALSE);
      g_return_val_if_fail (*a->message_id.str =='<', FALSE);

      g_return_val_if_fail (pstring_is_valid (&a->references), FALSE);
      g_return_val_if_fail (a->references.len==0 || *a->references.str =='<', FALSE);

      g_return_val_if_fail (a->number!=0, FALSE);

      g_return_val_if_fail (a->threads != DEAD_POINTER, FALSE);
      g_return_val_if_fail (a->parent != DEAD_POINTER, FALSE);
      g_return_val_if_fail (pstring_is_valid (&a->author_addr), FALSE);
      g_return_val_if_fail (pstring_is_valid (&a->author_real), FALSE);

      return TRUE;
}

gboolean
articles_are_valid (const Article ** a, int qty)
{
      int i;

      /* sanity clause */
      g_return_val_if_fail (qty>=0, FALSE);
      g_return_val_if_fail (qty==0 || a!=NULL, FALSE);

      /* check each article */
      for (i=0; i<qty; ++i)
            g_return_val_if_fail (article_is_valid (a[i]), FALSE);

      return TRUE;
}

gboolean
articles_are_valid_in_group (const Article ** a, int qty)
{
      int i;

      /* sanity clause */
      g_return_val_if_fail (articles_are_valid(a,qty), FALSE);

      /* check each article */
      if (qty > 0) {
            Group * group = a[0]->group;
            for (i=0; i<qty; ++i)
                  g_return_val_if_fail (a[i]->group == group, FALSE);
      }

      return TRUE;
}

/***
****  EVENTS
***/

PanCallback*
article_get_articles_changed_callback (void)
{
      static PanCallback * cb = NULL;
      if (cb==NULL) cb = pan_callback_new ();
      return cb;
}

static void
fire_articles_changed (Group              * group,
                       Article           ** articles,
                       int                  article_qty,
                       ArticleChangeType    type)
{
      ArticleChangeEvent e;
      debug_enter ("fire_articles_changed");

      g_return_if_fail (group!=NULL);
      g_return_if_fail (articles!=NULL);
      g_return_if_fail (article_qty>0);
      g_return_if_fail (type==ARTICLE_CHANGED_READ || type==ARTICLE_CHANGED_DIRTY || type==ARTICLE_CHANGED_NEW);

      e.group = group;
      e.articles = articles;
      e.article_qty = article_qty;
      e.type = type;
      pan_callback_call (article_get_articles_changed_callback(), &e, NULL);

      debug_exit ("fire_articles_changed");
}

Generated by  Doxygen 1.6.0   Back to index