/* OdpKiller main code */

// v 0.17 FIXED: Now correctly works with multiple spaces between "Re:" and "Odp:". Bug
//               reported by Piotr Sobolewski.


#define __NOLIBBASE__

#include <proto/dos.h>
#include <proto/exec.h>
#include <proto/utility.h>
#include <proto/rexxsyslib.h>

struct FolderPath
 {
  struct MinNode Node;
  char *Path;
  long *Number;
 };

struct Library *SysBase, *DOSBase, *UtilityBase, *RexxSysBase;

char *Yam2nnConfig, *YamFolders;
struct MinList Folders;
void *ListPool;

//--- CONSTANTS ------------------------------------------------------------------------------

#define OK                                   0
#define ERR_DOS_LIMIT                     1000
#define ERR_FAILED_TO_OPEN_YAM2NN_CONFIG  1000
#define ERR_YAM2NN_CANT_BE_RUN            1001
#define ERR_CONFIG_FILE_READ              1002
#define ERR_NO_UTILITY_LIB                1003
#define ERR_NO_REXX_LIB                   1004
#define ERR_MSGPORT_CREATION              1005
#define ERR_MAIL_FOLDER_LOCK              1006
#define ERR_MAILDIR_EXAMINE               1007
#define ERR_NO_REXXSYSLIB_LIB             1008
#define ERR_NO_REXX_REPLYPORT             1009
#define ERR_NO_COMMUNICATION_WITH_YAM     1010
#define ERR_FCONFIG_FORMAT_NOT_SUPPORTED  1011

char *VString = "$VER: OdpKiller 0.17 (21.2.2000) BLABLA PRODUCT";

char *PrgName = "OdpKiller";
char *ErrMsgs[] =
 {
  "can't open Yam2NN configuration file",
  "can't run Yam2NN (not in command path?)",
  "error loading Yam2NN configuration file",
  "can't open utility.library v37 or newer",
  "can't open rexxsyslib.library v36 or newer",
  "can't create message port (out of memory or signals)",
  "can't lock newsgroup directory",
  "can't examine newsgroup directory",
  "can't open rexxsyslib.library v36",
  "can't create message port for communication with YAM",
  "can't send ARexx commands to YAM (YAM is not running?)",
  "\042.fconfig\042 file is in format unsupported by OdpKiller"
 };

//
//--- ShowError () ---------------------------------------------------------------------------

void ShowError (long errnum)
 {
  if (errnum < ERR_DOS_LIMIT) PrintFault (errnum, PrgName);
  else Printf ("%s: %s.\n", (long)PrgName, (long)ErrMsgs[errnum - ERR_DOS_LIMIT]);
 }

//
//--- RunYam2NN() ----------------------------------------------------------------------------

long RunYam2NN (void)
 {
//  return TRUE;
  return (Execute ("Yam2NN", NULL, NULL));
 }

//
//--- GetFileLenght() ------------------------------------------------------------------------

long GetFileLenght (BPTR file)
 {
  Seek (file, 0, OFFSET_END);
  return (Seek (file, 0, OFFSET_BEGINNING));
 }

//
//--- LoadYam2NNConfig() ---------------------------------------------------------------------

long LoadYam2NNConfig (void)
 {
  long error = ERR_CONFIG_FILE_READ, cfglen;
  BPTR cfgfile;

  if (cfgfile = Open ("YAM:Yam2NN.config", MODE_OLDFILE))
   {
    if ((cfglen = GetFileLenght (cfgfile)) > 0)
     {
      if (Yam2nnConfig = AllocVec (cfglen + 1, MEMF_ANY))
       {
        if ((Read (cfgfile, Yam2nnConfig, cfglen)) == cfglen)
         {
          Yam2nnConfig[cfglen] = 0x00;                      /* end of file marker */
          if (ListPool = CreatePool (MEMF_ANY, 10 * sizeof (struct FolderPath),
           sizeof (struct FolderPath)))
           {
            Folders.mlh_Head = (struct MinNode*)&Folders.mlh_Tail;    /* list initialization */
            Folders.mlh_Tail = NULL;
            Folders.mlh_TailPred = (struct MinNode*)&Folders.mlh_Head;
            error = OK;
           }
          else error = ERROR_NO_FREE_STORE;
         }
       }
      else error = ERROR_NO_FREE_STORE;
     }
    Close (cfgfile);
   }
  return error;
 }

//--- SkipToEndOfLine() ----------------------------------------------------------------------

char *SkipToEndOfLine (char *pos)
 {
  while ((*pos != 0x0A) && (*pos)) pos++;
  if (*pos) return ++pos;
  else return pos;
 }

//
//--- SkipToNewWord() ------------------------------------------------------------------------

char *SkipToNewWord (char *pos)
 {
  while ((*pos != 0x20) && (*pos != 0x09) && (*pos != 0x0A) && (*pos)) pos++;
  if (*pos) while ((*pos == 0x20) || (*pos == 0x09) || (*pos == 0x0A)) pos++;
  return pos;
 }

//
//--- ParseYam2NNConfig() --------------------------------------------------------------------
// Parses loaded Yam2NN configuration file and creates a list of newsgroups folders to search.

void ParseYam2NNConfig (void)
 {
  char *parsepos = Yam2nnConfig;
  char *path;
  struct FolderPath *newfolder;

  while (*parsepos)
   {
    if (Strnicmp (parsepos, "newsgroups: ", 12) == 0)
     {
      parsepos = SkipToNewWord (SkipToNewWord (parsepos));    /* skipping two words */
      path = parsepos;
      parsepos = SkipToNewWord (parsepos);                    /* put 0 after path */
      if (Strnicmp ("active ", parsepos, 7) == 0)             /* only check active groups */
       {
        if (*parsepos) parsepos[-1] = 0x00;
        if (newfolder = AllocPooled (ListPool, sizeof (struct FolderPath)))
         {
          newfolder->Path = path;
          AddHead ((struct List*)&Folders, (struct Node*)&newfolder->Node);
         }
        else break;
       }
     }
    parsepos = SkipToEndOfLine (parsepos);
   }
  return;
 }

//--- FixSubjectLine() ------------------------------------------------------------------------
// Function actually removes "Odp:" from message subject.

void shift_string_left (char *string, long shift)
 {
  long index = 0;

  while (string[index + shift])
   {
    string[index] = string[index+shift];
    index++;
   }
  string[index] = 0x00;
  return;
 }

long FixSubjectLine (char *line)
 {
  long reduced = 0;
  long reply = FALSE;           /* reply detector */
  char *re = "Re: ";
  char *pattern_quoted_printable = "=[?]#?[?]Q[?]#?";
  char *pattern_base_64 = "=[?]#?[?]B[?]#?";
  char pattern_parsed[32];

  line = SkipToNewWord (line);          /* let's start from the beginning */

  /* check the subject against it is coded as quoted-printable */

  ParsePatternNoCase (pattern_quoted_printable, pattern_parsed, 32);
  if (MatchPatternNoCase (pattern_parsed, line)) Printf ("Quoted printable encoded\n");

  /* check the subject against it is coded as base 64 */

  ParsePatternNoCase (pattern_base_64, pattern_parsed, 32);
  if (MatchPatternNoCase (pattern_parsed, line)) Printf ("Base 64 encoded\n");

  while (*line)
   {
    if (Strnicmp (re, line, 4) == 0)
     {
      if (reply)
       {
        shift_string_left (line, 4);
        reduced += 4;
       }
      else
       {
        line += 4;
        reply = TRUE;
       }
     }
    else if (Strnicmp ("Odp: ", line, 5) == 0)
     {
      if (reply)
       {
        shift_string_left (line, 5);
        reduced += 5;
       }
      else
       {
        CopyMem (re, line, 4);
        line += 4;
        shift_string_left (line, 1);
        reduced++;
        reply = TRUE;
       }
     }
    else if (*line == 0x20)
     {
      shift_string_left (line, 1);    /* remove spaces between Re: & Odp: */
      reduced++;
     }
    else break;
   }
  return reduced;
 }

//--- FixPost() -------------------------------------------------------------------------------

long FixPost (char *filename, long filesize)
 {
  long error = OK;
  long subj_start = 0;          /* position of 'Subject:' line in a file */
  long rest_of_post;            /* position of the next line after 'Subject:' one */
  long reduced_chars;
  BPTR post;
  char *postline;

  if (postline = AllocMem (2048, MEMF_ANY))
   {
    if (post = Open (filename, MODE_OLDFILE))
     {
      while (FGets (post, postline, 2046))
       {
        if (Strnicmp ("Subject: ", postline, 9) == 0)
         {
          reduced_chars = FixSubjectLine (postline);
          rest_of_post = Seek (post, subj_start, OFFSET_BEGINNING);

          /* Line is always shorter or the same lenght, so I can write fixed line over old one */
          /* directly */

          if (Write (post, postline, strlen (postline)) != strlen (postline)) break;

          /* Check if I need to rewrite the rest of file. If no character was skipped I have */
          /* nothing more to do here. If a reduction has been preformed I rewrite the rest of */
          /* file using 2kB buffer allocated above */

          if (reduced_chars > 0)
           {
            long bytes_to_move = filesize - rest_of_post;
            long packet;

            while (bytes_to_move > 0)
             {
              packet = (bytes_to_move > 2048) ? 2048 : bytes_to_move;
              Seek (post, -bytes_to_move, OFFSET_END);
              if (Read (post, postline, packet) != packet) break;
              Seek (post, -packet - reduced_chars, OFFSET_CURRENT);
              if (Write (post, postline, packet) != packet) break;
              bytes_to_move -= packet;
             }

            /* Break the (external) loop if any bytes left unmoved (it means I/O error). */

            if (bytes_to_move > 0) break;

            /* I should cut last [reduced_chars] bytes off the file now. */

            SetFileSize (post, -reduced_chars, OFFSET_END);
            SetIoErr (OK);
           }
          break;
         }
        subj_start = Seek (post, 0, OFFSET_CURRENT);
       }
      error = IoErr();
      Close (post);
     }
    else error = IoErr ();
    FreeMem (postline, 2048);
   }
  else error = ERROR_NO_FREE_STORE;
  return error;
 }

//
//--- FixFolder() ----------------------------------------------------------------------------
// Function fixes all new messages in the folder.

long FixFolder (char *fldpath)
 {
  long error = OK;
  struct FileInfoBlock *fib;
  BPTR dirlock, olddir;
  char *pattern = "[0-9][0-9][0-9][0-9][0-9].[0-9][0-9][0-9]";
  char parsed[86];

  ParsePatternNoCase (pattern, parsed, 86);

  if (fib = AllocDosObject (DOS_FIB, NULL))
   {
    if (dirlock = Lock (fldpath, ACCESS_READ))
     {
      olddir = CurrentDir (dirlock);
      if (Examine (dirlock, fib))
       {
        while (ExNext (dirlock, fib))
         {
          if ((fib->fib_DirEntryType < 0) && ((strcmp ("N", fib->fib_Comment) == 0) ||
           (strcmp ("", fib->fib_Comment) == 0)))
           {
            if (MatchPatternNoCase (parsed, fib->fib_FileName))
             {
              if ((error = FixPost (fib->fib_FileName, fib->fib_Size)) != OK)
               {
                SetIoErr (error);
                break;
               }
             }
           }
         }
        if ((error = IoErr ()) == ERROR_NO_MORE_ENTRIES) error = OK;
       }
      else error = ERR_MAILDIR_EXAMINE;
      CurrentDir (olddir);
      UnLock (dirlock);
     }
    else error = ERR_MAIL_FOLDER_LOCK;
    FreeDosObject (DOS_FIB, fib);
   }
  else error = ERROR_NO_FREE_STORE;
  return error;
 }

//--- SendYamCommand() ------------------------------------------------------------------------
// Sends ARexx command (already prepared RexxMsg) to YAM and waits for completion.

long SendYamCommand (struct MsgPort *reply_port, struct RexxMsg *message)
 {
  struct MsgPort *yam_port;

  if (FillRexxMsg (message, 1, 0))
   {
    Forbid ();
    if (yam_port = FindPort ("YAM")) PutMsg (yam_port, (struct Message*)message);
    Permit ();
    if (yam_port)
     {
      WaitPort (reply_port);
      GetMsg (reply_port);
      return OK;
     }
    ClearRexxMsg (message, 1);
   }
  return ERR_NO_COMMUNICATION_WITH_YAM;
 }

//--- GetKeywordValue() -----------------------------------------------------------------------
// 'line' is expected to contain "keyword=value<LF>" with any white spaces before keyword,
// between keyword and '=', between '=' and value and after value. Any WS inside value are
// treaten as parts of the value. The same for WS inside keyword. Therefore it is possible
// to have eg. "   My Keyword   =    My Value    <LF>", where "My Keyword" is the keyword
// and "My Value" is the value. Value is copied into 'store', size isn't checked (store
// must be long enough). 'line' is modified. Keyword is not case sensitive.

long GetKeywordValue (char *line, char *keyword, char *store)
 {
  char *p = line;
  char *kw, *vl;

  while (*p == 0x09 || *p == 0x20) p++;             /* skip WS before keyword */
  kw = p;                                           /* remember keyword start */
  if (*p == 0x0A) return FALSE;                     /* if line is empty (contains only WS) */
  if (*p == 0x3D) return FALSE;                     /* if no keyword */
  while ((*p != 0x3D) && (*p != 0x0A)) p++;         /* skip to '=', stop if LF */
  if (*p == 0x0A) return FALSE;                     /* if line contains no '=' */
  vl = p;                                           /* remember '=' and back before it */
  vl++;                                             /* probably value starts after '=' */
  p--;
  while (*p == 0x09 || *p == 0x20) p--;             /* skip WS between '=' and keyword */
  *(++p) = 0x00;                                    /* place 0 after keyword */
  while (*vl == 0x09 || *vl == 0x20) vl++;          /* skip WS between '=' and value */
  if (*vl == 0x0A) return FALSE;                    /* if no value */
  p = vl;
  while (*p != 0x0A) p++;                           /* go to EOL */
  p--;                                              /* back to char before EOL */
  while (*p == 0x09 || *p == 0x20) p--;             /* skip WS after value */
  *(++p) = 0x00;                                    /* place 0 after value */
  if (Stricmp (keyword, kw)) return FALSE;          /* if keyword not right */
  strcpy (store, vl);                               /* store the value */
  return TRUE;
 }

//--- ReadFolderName() ------------------------------------------------------------------------
// Reads folder name from ".fconfig" file in folder directory. Name is copied to table pointed
// by 'store'.

long ReadFolderName (char *folder_dir, char *store)
 {
  long error = OK;
  char path[512];   // this table is used twice, first as file path, second as line buffer
  char *name;       // it serves as parameter table for ReadArgs()
  BPTR fconfig;
  struct RDArgs args, *args2;

  strncpy (path, folder_dir, 512);
  AddPart (path, ".fconfig", 512);
  if (fconfig = Open (path, MODE_OLDFILE))
   {
    if (FGets (fconfig, path, 512))
     {
      if (strncmp ("YFC1", path, 4) == 0)
       {
        while (FGets (fconfig, path, 512))
         {
          if (GetKeywordValue (path, "NAME", store)) break;
         }
        error = IoErr ();
       }
      else error = ERR_FCONFIG_FORMAT_NOT_SUPPORTED;
     }
    else error = IoErr ();
    Close (fconfig);
   }
  else error = IoErr ();
  return error;
 }

//--- UpdateFolderIndex() ---------------------------------------------------------------------
// Function determines folder name from its path, next sends ARexx commands refreshing folder
// to YAM.

long UpdateFolderIndex (struct MsgPort *reply_port, char *folder_path)
 {
  long error;
  char folder_command[36], folder_name[24];
  struct RexxMsg *message;

  // Read folder name from ".fconfig" file.

  if ((error = ReadFolderName (folder_path, folder_name)) == OK)
   {
    // Update folder index using YAM ARexx port.

    if (message = CreateRexxMsg (reply_port, NULL, NULL))
     {
      message->rm_Action = RXCOMM;

      // Switch to given folder.

      sprintf (folder_command, "SetFolder \042%s\042", (long)folder_name);
      message->rm_Args[0] = folder_command;
      error = SendYamCommand (reply_port, message);

      // Update folder index.

      message->rm_Args[0] = "MailUpdate";
      error = SendYamCommand (reply_port, message);

      DeleteRexxMsg (message);
     }
    else error = ERR_NO_COMMUNICATION_WITH_YAM;
   }
  return error;
 }

// Locks YAM GUI and displays info text.

long LockYamGui (struct MsgPort *reply_port)
 {
  long error;
  struct RexxMsg *message;

  if (message = CreateRexxMsg (reply_port, NULL, NULL))
   {
    message->rm_Action = RXCOMM | RXFF_RESULT;
    message->rm_Args[0] = "AppBusy \042OdpKiller is doing its dirty job now...\042";
    error = SendYamCommand (reply_port, message);

    DeleteRexxMsg (message);
   }
  else error = ERR_NO_COMMUNICATION_WITH_YAM;
  return error;
 }

// Unlocks YAM GUI.

long UnlockYamGui (struct MsgPort *reply_port)
 {
  long error;
  struct RexxMsg *message;

  if (message = CreateRexxMsg (reply_port, NULL, NULL))
   {
    message->rm_Action = RXCOMM;
    message->rm_Args[0] = "AppNoBusy";
    error = SendYamCommand (reply_port, message);
    DeleteRexxMsg (message);
   }
  else error = ERR_NO_COMMUNICATION_WITH_YAM;
  return error;
 }

// Opens ulility library and rexxsyslib library.

long OpenLibs (void)
 {
  if (!(UtilityBase = OpenLibrary ("utility.library", 37))) return ERR_NO_UTILITY_LIB;
  if (!(RexxSysBase = OpenLibrary ("rexxsyslib.library", 36))) return ERR_NO_REXXSYSLIB_LIB;
  return OK;
 }

// Closes libraries if opened.

void CloseLibs (void)
 {
  if (UtilityBase) CloseLibrary (UtilityBase);
  if (RexxSysBase) CloseLibrary (RexxSysBase);
  return;
 }

//--- Main() ---------------------------------------------------------------------------------

long Main (long wb)
 {
  long error = OK;
  struct FolderPath *folder;
  struct MsgPort *rexx_replyport;

  if ((error = OpenLibs ()) == OK)
   {
    if (RunYam2NN ())
     {
      if ((error = LoadYam2NNConfig ()) == OK)
       {
        ParseYam2NNConfig ();
        if (rexx_replyport = CreateMsgPort ())
         {
          if ((error = LockYamGui (rexx_replyport)) == OK)
           {
            for (folder = (struct FolderPath*)Folders.mlh_Head; folder->Node.mln_Succ;
            folder = (struct FolderPath*)folder->Node.mln_Succ)
             {
              if ((error = FixFolder (folder->Path)) != OK) break;
              UpdateFolderIndex (rexx_replyport, folder->Path);
             }
            error = UnlockYamGui (rexx_replyport);
           }
          DeleteMsgPort (rexx_replyport);
         }
        else error = ERR_NO_REXX_REPLYPORT;
        if (Yam2nnConfig) FreeVec (Yam2nnConfig);    /* memory allocated in ReadYam2NNConfig() */
        if (ListPool) DeletePool (ListPool);         /* memory for folders paths list */
       }
     }
    else error = ERR_YAM2NN_CANT_BE_RUN;
   }
  CloseLibs ();

  if (error)
   {
    ShowError (error);
    return 20;
   }
  else return 0;
 }


