//---------------------------------------------------------------------------
// =====================
// Cancel Message Hunter
// =====================
//
// by JT & HZ
// using Borland C++Builder 5 PRO
// April 10, 2000
//---------------------------------------------------------------------------

#include "Global.h"
#pragma hdrstop

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

#pragma package(smart_init)
#pragma resource "*.dfm"

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

// Function to set initial directory for ShBrowseForFolder()
int CALLBACK BrowseCallbackProc(HWND, UINT, LPARAM, LPARAM);

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

// Time to display hints in msecs
static const int HUNT_HINTHIDEPAUSE      = 10000;

// Color for hints
static const TColor HUNT_HINTCOLOR       = clWhite;

// Registry storage
static const String HUNT_REGKEY          = "SOFTWARE\\hz37";
static const HKEY   HUNT_REGROOT         = HKEY_CURRENT_USER;

// Registry identifiers
static const String HUNT_NEWSSERVER      = "Newsserver";
static const String HUNT_USER            = "User";
static const String HUNT_NEWSGROUP       = "Newsgroup";
static const String HUNT_CANCELGROUP     = "Cancelgroup";
static const String HUNT_STRING          = "Huntstring";
static const String HUNT_STORAGE         = "Storage directory";
static const String HUNT_INTERVAL        = "Interval";
static const String HUNT_TOPMOST         = "Topmost";

// Registry defaults
static const String HUNT_CANCEL_DEF      = "control.cancel";
static const String HUNT_STORAGE_DEF     = "C:\\";
static const int    HUNT_INTERVAL_DEF    = 5; // minutes
static const int    HUNT_TOPMOST_DEF     = 0;

// Various constants to avoid literals in code
static const int HUNT_SECONDS_PER_MINUTE = 60;
static const int HUNT_NNTP_TIMEOUT       = 60000;
static const int HUNT_BAD_FILE_HANDLE    = -1;

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

THuntForm* HuntForm;

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

// CALLBACK to set initial directory for ShBrowseForFolder()

#pragma argsused
int CALLBACK BrowseCallbackProc(
    HWND WindowHandle,
    UINT Message,
    LPARAM UserParam1, // WPARAM in disguise; shlobj.h defines it as LPARAM
    LPARAM UserParam2){

  if(Message == BFFM_INITIALIZED){
    PCSTR PrevDir = reinterpret_cast <PCSTR> (UserParam2);

    if(DirectoryExists(PrevDir)){
      SendMessage(
        WindowHandle,
        BFFM_SETSELECTION,
        // The lParam parameter is the PIDL of the folder to select if
        // wParam is FALSE, or it is the path of the folder otherwise.
        static_cast <LPARAM> (true),
        UserParam2
      );
    }
  }

  return 0;

}

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

// Constructor; just initialize some fields

__fastcall THuntForm::THuntForm(TComponent* Owner):
    TForm(Owner),
    StorageDirectory(""),
    HuntMode(E_HUNT_Unarmed){

}

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

// Retrieve String from Registry

String __fastcall THuntForm::RegGet(
    const String Name,
    const String DefaultValue){

  String Value = DefaultValue;

  TRegistry* Registry = new TRegistry();
  Registry->RootKey = HUNT_REGROOT;
  Registry->OpenKey(HUNT_REGKEY, true);

  try{
    if(Registry->ValueExists(Name))
      Value = Registry->ReadString(Name);
  }
  catch(...){
  }

  Registry->CloseKey();

  delete Registry;

  return Value;

}

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

// Retrieve int from Registry

int __fastcall THuntForm::RegGet(const String Name, const int DefaultValue){

  int Value = DefaultValue;

  TRegistry* Registry = new TRegistry();
  Registry->RootKey = HUNT_REGROOT;
  Registry->OpenKey(HUNT_REGKEY, true);

  try{
    if(Registry->ValueExists(Name))
      Value = Registry->ReadInteger(Name);
  }
  catch(...){
  }

  Registry->CloseKey();

  delete Registry;

  return Value;

}

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

// Write String to Registry

void __fastcall THuntForm::RegPut(const String Name, const String Value){

  TRegistry* Registry = new TRegistry();
  Registry->RootKey = HUNT_REGROOT;
  Registry->OpenKey(HUNT_REGKEY, true);

  try{
    Registry->WriteString(Name, Value);
  }
  catch(...){
  }

  Registry->CloseKey();

  delete Registry;

}

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

// Write int to Registry

void __fastcall THuntForm::RegPut(const String Name, const int Value){

  TRegistry* Registry = new TRegistry();
  Registry->RootKey = HUNT_REGROOT;
  Registry->OpenKey(HUNT_REGKEY, true);

  try{
    Registry->WriteInteger(Name, Value);
  }
  catch(...){
  }

  Registry->CloseKey();

  delete Registry;

}

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

void __fastcall THuntForm::FormCreate(TObject *Sender){

  // up the time to display flyby hints
  Application->HintHidePause = HUNT_HINTHIDEPAUSE;

  // change color for hints
  Application->HintColor     = HUNT_HINTCOLOR;

  // update user config stuff from registry
  NewsserverEdit->Text  = RegGet(HUNT_NEWSSERVER,  "");
  UserEdit->Text        = RegGet(HUNT_USER,        "");
  NewsgroupEdit->Text   = RegGet(HUNT_NEWSGROUP,   "");
  CancelgroupEdit->Text = RegGet(HUNT_CANCELGROUP, HUNT_CANCEL_DEF);
  HuntEdit->Text        = RegGet(HUNT_STRING,      "");

  StorageDirectory      = RegGet(HUNT_STORAGE,
                          HUNT_STORAGE_DEF);
  // make sure this storage directory is valid
  if(!DirectoryExists(StorageDirectory))
    StorageDirectory = HUNT_STORAGE_DEF;

  bool Topmost = RegGet(HUNT_TOPMOST, HUNT_TOPMOST_DEF);
  FormStyle = Topmost ? fsStayOnTop : fsNormal;
  TopmostCheckBox->Checked = Topmost;

  TimerIntervalUpDown->Position = static_cast <short>
                                  (RegGet(HUNT_INTERVAL,
                                  HUNT_INTERVAL_DEF));

  // if newsserver if found in registry, guess we need a password
  // so set focus to that particular control
  if(NewsserverEdit->Text.Length() > 0)
    HuntForm->ActiveControl = PasswordEdit;

  // update display
  EditsChange(0);
  StorageChange();
  UpdateStatus();

}

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

// Remember user settings (except for password) when program ends

void __fastcall THuntForm::FormDestroy(TObject *Sender){

  // update user config stuff in registry for next time
  RegPut(HUNT_NEWSSERVER,  NewsserverEdit->Text);
  RegPut(HUNT_USER,        UserEdit->Text);
  RegPut(HUNT_NEWSGROUP,   NewsgroupEdit->Text);
  RegPut(HUNT_CANCELGROUP, CancelgroupEdit->Text);
  RegPut(HUNT_STRING,      HuntEdit->Text);
  RegPut(HUNT_STORAGE,     StorageDirectory);
  RegPut(HUNT_INTERVAL,    TimerIntervalUpDown->Position);
  RegPut(HUNT_TOPMOST,     (TopmostCheckBox->Checked ? 1 : 0));

}
//---------------------------------------------------------------------------

// Enable Arm-button if all required variables have been filled out

void __fastcall THuntForm::EditsChange(TObject *Sender){

  if((NewsserverEdit->Text.Length()  > 0) &&
     (UserEdit->Text.Length()        > 0) &&
     (PasswordEdit->Text.Length()    > 0) &&
     (NewsgroupEdit->Text.Length()   > 0) &&
     (CancelgroupEdit->Text.Length() > 0) &&
     (HuntEdit->Text.Length()        > 0))
    ArmButton->Enabled = true;
  else
    ArmButton->Enabled = false;

}

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

// Update interface after storage directory has changed

void __fastcall THuntForm::StorageChange(){

  StorageDirectoryEdit->Text = StorageDirectory;

}

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

// Find a new directory to store messages for offline reading

void __fastcall THuntForm::DirectoryButtonClick(TObject *Sender){

  LPMALLOC MallocObject;

  if(SHGetMalloc(&MallocObject) != NOERROR){
    ShowMessage("SHGetMalloc failed");
    return;
  }

  BROWSEINFO BrowseInfo;
  BrowseInfo.hwndOwner      = Handle;
  BrowseInfo.pidlRoot       = NULL;
  BrowseInfo.pszDisplayName = "";
  BrowseInfo.lpszTitle      = "Locate storage directory for usenet messages";
  BrowseInfo.ulFlags        = BIF_RETURNFSANCESTORS | BIF_RETURNONLYFSDIRS |
                              BIF_DONTGOBELOWDOMAIN;
  BrowseInfo.lpfn           = reinterpret_cast <BFFCALLBACK>
                              (BrowseCallbackProc);
  BrowseInfo.lParam         = reinterpret_cast <LPARAM>
                              (StorageDirectory.c_str());

  char NewWorkDir[MAX_PATH] = "";

  LPITEMIDLIST ItemIDList   = SHBrowseForFolder(&BrowseInfo);
  if(ItemIDList != NULL)
    SHGetPathFromIDList(ItemIDList, NewWorkDir);

  MallocObject->Free(ItemIDList);
  MallocObject->Release();

  if(DirectoryExists(NewWorkDir)){
    StorageDirectory = NewWorkDir;
    StorageChange();
  }

}
//---------------------------------------------------------------------------

// The Timer fires every second, but TimerElapsed determines if
// action should be taken. Currently, only if...
// HuntMode == E_HUNT_HuntingForMessage  ...or...
// HuntMode == E_HUNT_HuntingForCancelMessage
// ...we will actually jump into action.

void __fastcall THuntForm::Timer1Timer(TObject *Sender){

  static int CurrentSecond = 0;

  ++CurrentSecond;

  // update ProgressBar to show when next polling will occur
  double PercentageDone = (static_cast <double> (CurrentSecond) /
                           static_cast <double> (TimerElapsed)) * 100.0;
  ProgressBar1->Position = 100 - static_cast <short> (PercentageDone);
  if(CurrentSecond >= TimerElapsed){
    ProgressBar1->Position = 0;
    CurrentSecond = 0;
    Timer1->Enabled = false; // stall the timer until function has returned
                             // to avoid starting again before we're finished
    try{
      if(HuntMode == E_HUNT_HuntingForMessage){
        ScanNewsgroup(
          NewsgroupEdit->Text,
          ScanBodyForString,
          HuntEdit->Text
        );
      }
      else if(HuntMode == E_HUNT_HuntingForCancelMessage){
        ScanNewsgroup(
          CancelgroupEdit->Text,
          ScanHeaderForString,
          CancelMessageId
        );
      }
    }
    catch(...){
      // ignore exception
    }
    if(HuntMode != E_HUNT_Finished){
      ProgressBar1->Position = 100;
      Timer1->Enabled = true; // okay, start again
      ActionLabel->Caption = "Counting down";
    }
    else
      ActionLabel->Caption = "Done!";
  }

}

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

void __fastcall THuntForm::ArmButtonClick(TObject *Sender){

  HuntMode           = E_HUNT_HuntingForMessage;
  TimerElapsed       = TimerIntervalUpDown->Position * HUNT_SECONDS_PER_MINUTE;
  UpdateStatus();

  EnableControls(false);

  ProgressBar1->Position = 100;

  Timer1->Enabled = true;

}
//---------------------------------------------------------------------------

void __fastcall THuntForm::KillButtonClick(TObject *Sender){

  ProgressBar1->Position = 0;
  
  Timer1->Enabled = false;

  // kill connection if we are still in the middle of a connection
  if(Nntp->Connected){
    try{
      Nntp->Disconnect();
    }
    catch(...){
      // ignore exception
    }
  }

  EnableControls();
  HuntMode = E_HUNT_Unarmed;
  UpdateStatus();

}

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

// Event called by NNTP object if an invalid article is retrieved

void __fastcall THuntForm::NntpInvalidArticle(TObject *Sender){

  // flag error so ScanNewsgroup() knows the article could not be retrieved
  IsValidArticle = false;

}

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

// extract the index of the current article from its header
// e.g. Xref: news1.xs4all.nl alt.comp.shareware.programmer:26036
// Some indices have been dropped from the newsserver, so we can't
// simply walk from Nntp->LoMessage to Nntp->HiMessage.

void __fastcall THuntForm::NntpHeaderList(TObject *Sender){

  int ArticleId = Nntp->HeaderRecord->PrArticleId;
  ArticleIndices.push_back(ArticleId);

}

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

// Extract actual Message-ID from string like this
// Message-ID: <38e382c0.1881059@news>
// This begs for a regular expression

String __fastcall THuntForm::ExtractMessageId(TStringList* Header){

  String Pattern                    = "^Message-ID:\\s<(.+)>$";
  const int CompileOptions          = PCRE_ANCHORED | PCRE_CASELESS |
                                      PCRE_DOLLAR_ENDONLY;
  const char* ErrorPointer          = 0;
  int ErrorOffset                   = 0;
  const unsigned char* TablePointer = pcre_maketables();

  pcre* CompiledPattern = pcre_compile(
    Pattern.c_str(),
    CompileOptions,
    &ErrorPointer,
    &ErrorOffset,
    TablePointer
  );

  const int MatchVectorSize        = 6; // n == 1, so (n + 1) * 3 == 6
  int MatchVector[MatchVectorSize] = { 0 };
  const int ExecOptions            = 0;
  bool Found                       = false;
  String ReturnValue               = "";

  for(int Idx = 0; (Idx < Header->Count) && (!Found); ++Idx){
    String MatchMe = Trim(Header->Strings[Idx]);

    int ExecResult = pcre_exec(
      CompiledPattern,
      0,
      MatchMe.c_str(),
      MatchMe.Length(),
      ExecOptions,
      MatchVector,
      MatchVectorSize
    );

    if(ExecResult < 0)
      continue; // no match or error

    // else we have a match and we're done searching
    int Offset1         = MatchVector[2];
    int Offset2         = MatchVector[3];
    ReturnValue         = MatchMe.SubString(Offset1 + 1, Offset2 - Offset1);
    Found               = true;
  }

  free(CompiledPattern);

  return ReturnValue;

}

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

void __fastcall THuntForm::UpdateStatus(){

  String Status;

  switch(HuntMode){
    case E_HUNT_Unarmed:
      Status = "Waiting for input";
      break;
    case E_HUNT_HuntingForMessage:
      Status = "Message hunting";
      break;
    case E_HUNT_HuntingForCancelMessage:
      Status = "Cancel hunting: " + CancelMessageId;
      break;
    case E_HUNT_Finished:
      Status = "Cancel message found and saved";
      break;
    case E_HUNT_Invalid:
      // fall thru
    default:
      Status = "Invalid mode encountered";
  }

  ModeLabel->Caption = Status;

}

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

// Disable/Enable user config controls
// Status gets a default value (true) in header

void __fastcall THuntForm::EnableControls(bool Status){

  NewsserverEdit->Enabled      = Status;
  UserEdit->Enabled            = Status;
  PasswordEdit->Enabled        = Status;
  NewsgroupEdit->Enabled       = Status;
  CancelgroupEdit->Enabled     = Status;
  HuntEdit->Enabled            = Status;
  TimerIntervalUpDown->Enabled = Status;
  ArmButton->Enabled           = Status;
  DirectoryButton->Enabled     = Status;
  KillButton->Enabled          = !Status;

}

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

// scan message BODY for the special string

bool __fastcall THuntForm::ScanBodyForString(
    const String Token,
    const String MessageId){

  if(Nntp->Body->Text.Pos(Token) == 0)
    return false;

  // message contains string!
  // Let's hunt for the cancel message now
  NewsgroupLabel->Caption = "";

  // remember the Message-ID so we can hunt it down in control.cancel
  CancelMessageId = MessageId;

  // save this message to a file
  SaveCurrentMessage("message.txt");

  // change mode
  HuntMode = E_HUNT_HuntingForCancelMessage;

  UpdateStatus();

  return true;

}

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

// scan message HEADER for our message ID

#pragma argsused // ignoring MessageId here
bool __fastcall THuntForm::ScanHeaderForString(
    const String Token,
    const String MessageId){

  if(Nntp->Header->Text.Pos(Token) == 0)
    return false;

  // this is the cancel message!

  // save this message to a file
  SaveCurrentMessage("cancel.txt");

  // change mode
  HuntMode = E_HUNT_Finished;

  UpdateStatus();

  return true;

}

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

bool __fastcall THuntForm::ScanNewsgroup(
    const String Newsgroup,
    THuntAction  HuntAction,
    const String Token){

  if(Nntp->Connected){
    try{
      Nntp->Disconnect();
    }
    catch(...){
      // ignore exception
    }
  }

  Nntp->Host     = NewsserverEdit->Text;
  Nntp->UserId   = UserEdit->Text;
  Nntp->Password = PasswordEdit->Text;
  Nntp->TimeOut  = HUNT_NNTP_TIMEOUT;

  ActionLabel->Caption = "Connecting...";
  try{
    Nntp->Connect();
  }
  catch(...){
    ActionLabel->Caption = "Connection failed";
    return false;
  }
  if(!Nntp->Connected){
    ActionLabel->Caption = "Connection failed";
    return false;
  }
  Nntp->SetGroup(Newsgroup);
  if(Nntp->SelectedGroup != Newsgroup){
    Caption = "Group not found (typo?)";
    ActionLabel->Caption = "Disconnecting";
    try{
      Nntp->Disconnect();
    }
    catch(...){
      // ignore exception
    }
    return false;
  }

  Animate1->Active  = true;
  Animate1->Visible = true;

  // get all articles
  ActionLabel->Caption    = "Getting articles";
  NewsgroupLabel->Caption = Newsgroup;

  // clear container of indices (GetArticleList() will refresh it)
  ArticleIndices.clear();

  // get all articles (something which might be improved)
  Nntp->GetArticleList(true, 0);

  for(TArticleIndicesIterator Idx = ArticleIndices.begin();
      Idx != ArticleIndices.end(); ++Idx){
    int ArticleIndex = *Idx;
    IsValidArticle = true;
    Nntp->GetArticle(ArticleIndex);
    if(IsValidArticle){
      String MessageId = ExtractMessageId(Nntp->Header);
      if(MessageId.Length() == 0)
        continue; // could not extract Message-ID (this should *never* happen)
      ActionLabel->Caption    = "Scanning message";
      IndexLabel->Caption     = IntToStr(ArticleIndex);
      MessageIdLabel->Caption = MessageId;
      if(HuntAction(Token, MessageId))
        break; // true returned, meaning: Token was found so we're done iterating
    }
  }
  ActionLabel->Caption    = "Disconnecting";
  IndexLabel->Caption     = "";
  MessageIdLabel->Caption = "";
  try{
    Nntp->Disconnect();
  }
  catch(...){
    // ignore exception
  }

  Animate1->Visible = false;
  Animate1->Active  = false;

  return true;

}

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

void __fastcall THuntForm::SaveCurrentMessage(const String ShortName){

  String FileName = StorageDirectory;
  int Length = FileName.Length();
  if(Length && (FileName[Length] != '\\'))
    FileName += "\\";
  FileName += ShortName;

  int FileHandle = FileCreate(FileName);
  if(FileHandle == HUNT_BAD_FILE_HANDLE)
    return; // could not create file

  // save header
  FileWrite(FileHandle, Nntp->Header->Text.c_str(), Nntp->Header->Text.Length());

  // save body
  FileWrite(FileHandle, Nntp->Body->Text.c_str(),   Nntp->Body->Text.Length());

  FileClose(FileHandle);

}

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

void __fastcall THuntForm::TopmostCheckBoxClick(TObject *Sender)
{

  HuntForm->FormStyle = (TopmostCheckBox->Checked? fsStayOnTop : fsNormal);
  
}

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


