From e5a89ae8331c9eaa1871503ae5f87b70d56ba85f Mon Sep 17 00:00:00 2001
From: Fred Gleason <fredg@paravelsystems.com>
Date: Mon, 23 Nov 2020 11:56:35 -0500
Subject: [PATCH] 2020-11-22 Fred Gleason <fredg@paravelsystems.com> 	*
 Added '--send-mail' and '--mail-per-file' options to rdimport(1).

Signed-off-by: Fred Gleason <fredg@paravelsystems.com>
---
 ChangeLog                   |   2 +
 docs/manpages/rdimport.xml  |  48 ++++++++
 utils/rdimport/Makefile.am  |   3 +-
 utils/rdimport/journal.cpp  | 235 ++++++++++++++++++++++++++++++++++++
 utils/rdimport/journal.h    |  52 ++++++++
 utils/rdimport/rdimport.cpp |  72 ++++++++---
 utils/rdimport/rdimport.h   |   6 +
 7 files changed, 402 insertions(+), 16 deletions(-)
 create mode 100644 utils/rdimport/journal.cpp
 create mode 100644 utils/rdimport/journal.h

diff --git a/ChangeLog b/ChangeLog
index 130a1c10..94f0d09b 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -20618,3 +20618,5 @@
 	Settings' dialog in rdadmin(1).
 	* Added a 'Notification E-Mail Addresses' control to the 'Group'
 	dialog in rdadmin(1).
+2020-11-22 Fred Gleason <fredg@paravelsystems.com>
+	* Added '--send-mail' and '--mail-per-file' options to rdimport(1).
diff --git a/docs/manpages/rdimport.xml b/docs/manpages/rdimport.xml
index 337af65a..3f05704d 100644
--- a/docs/manpages/rdimport.xml
+++ b/docs/manpages/rdimport.xml
@@ -256,6 +256,24 @@
       </listitem>
     </varlistentry>
 
+    <varlistentry>
+      <term>
+	<option>--mail-per-file</option>
+      </term>
+      <listitem>
+	<para>
+	  Send an e-mail message for each file processed, rather than
+	  a single message per run summarizing all actions taken. Implies
+	  the <command>--send-mail</command> switch.
+	</para>
+	<para>
+	  See the <command>--send-mail</command> switch (below) for more
+	  details about generating e-mailed reports from
+	  <command>rdimport</command><manvolnum>1</manvolnum>.
+	</para>
+      </listitem>
+    </varlistentry>
+
     <varlistentry>
       <term>
 	<option>--metadata-pattern=</option><replaceable>pattern</replaceable>
@@ -547,6 +565,32 @@
       </listitem>
     </varlistentry>
 
+    <varlistentry>
+      <term>
+	<option>--send-mail</option>
+      </term>
+      <listitem>
+	<para>
+	  Send e-mail to the address(es) specified in the destination group's
+	  <computeroutput>Notification E-Mail Addresses</computeroutput>
+	  setting in <command>rdadmin</command><manvolnum>1</manvolnum>
+	  summarizing the action(s) performed during the run.
+	  Each invocation of
+	  <command>rdimport</command><manvolnum>1</manvolnum> will
+	  potentially generate
+	  one message for all successful imports and another for all failed
+	  imports (but see the <option>--mail-per-file</option> switch
+	  (above) for a way to modify this behavior).
+	</para>
+	<para>
+	  NOTE: Rivendell uses the system's
+	  <command>sendmail</command><manvolnum>1</manvolnum> subsystem
+	  for originating e-mail. For many modern e-mail setups, further
+	  configuration of that subsystem may be necessary.
+	</para>
+      </listitem>
+    </varlistentry>
+
     <varlistentry>
       <term>
 	<option>--set-datetimes=</option><replaceable>start-datetime</replaceable>,<replaceable>end-datetime</replaceable>
@@ -989,6 +1033,10 @@
 
 <refsect1 id='see_also'><title>See Also</title>
 <para>
+  <citerefentry>
+    <refentrytitle>sendmail</refentrytitle><manvolnum>1</manvolnum>
+  </citerefentry>
+  <literal>,</literal>
   <citerefentry>
     <refentrytitle>rdexport</refentrytitle><manvolnum>1</manvolnum>
   </citerefentry>
diff --git a/utils/rdimport/Makefile.am b/utils/rdimport/Makefile.am
index 0fdfd8f4..ca5f6f57 100644
--- a/utils/rdimport/Makefile.am
+++ b/utils/rdimport/Makefile.am
@@ -27,7 +27,8 @@ moc_%.cpp:	%.h
 
 bin_PROGRAMS = rdimport
 
-dist_rdimport_SOURCES = markerset.cpp markerset.h\
+dist_rdimport_SOURCES = journal.cpp journal.h\
+                        markerset.cpp markerset.h\
                         rdimport.cpp rdimport.h
 
 nodist_rdimport_SOURCES = moc_rdimport.cpp
diff --git a/utils/rdimport/journal.cpp b/utils/rdimport/journal.cpp
new file mode 100644
index 00000000..92a01032
--- /dev/null
+++ b/utils/rdimport/journal.cpp
@@ -0,0 +1,235 @@
+// journal.cpp
+//
+// E-mail file importation actions
+//
+//   (C) Copyright 2020 Fred Gleason <fredg@paravelsystems.com>
+//
+//   This program is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU General Public License version 2 as
+//   published by the Free Software Foundation.
+//
+//   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., 675 Mass Ave, Cambridge, MA 02139, USA.
+//
+
+#include <rdapplication.h>
+#include <rdgroup.h>
+#include <rdsendmail.h>
+
+#include "journal.h"
+
+Journal::Journal(bool send_immediately)
+{
+  c_send_immediately=send_immediately;
+}
+
+
+void Journal::addSuccess(const QString &groupname,QString filename,
+			 unsigned cartnum,const QString &title)
+{
+  QString errors;
+  QString subject;
+  QString body;
+  RDGroup *group=new RDGroup(groupname);
+  QStringList addrs=
+    group->notifyEmailAddress().split(",",QString::SkipEmptyParts);
+
+  if(addrs.size()>0) {
+    filename=filename.split("/",QString::SkipEmptyParts).last();
+    if(c_send_immediately) {
+      subject=QObject::tr("Rivendell import for file")+": "+filename;
+      body+=QObject::tr("Rivendell File Import Report")+"\n";
+      body+="\n";
+      body+=QObject::tr("Import Success")+"\n";
+      body+="\n";
+      body+=QObject::tr("Filename")+": "+filename+"\n";
+      body+=QObject::tr("Submitted by")+": ";
+      if(rda->user()->emailAddress().isEmpty()) {
+	body+=rda->user()->name()+"\n";
+      }
+      else {
+	body+=rda->user()->emailAddress()+"\n";
+      }
+      body+=QObject::tr("Group")+": "+groupname+"\n";
+      body+=QObject::tr("Cart Number")+QString().sprintf(": %06u",cartnum)+"\n";
+      body+=QObject::tr("Cart Title")+": "+title+"\n";
+
+      if(!RDSendMail(&errors,subject,body,rda->system()->originEmailAddress(),
+		     addrs)) {
+	rda->syslog(LOG_WARNING,"email send failed [%s]",
+		    errors.toUtf8().constData());
+      }
+    }
+    else {
+      c_good_groups.push_back(groupname);
+      c_good_filenames.push_back(filename);
+      c_good_cart_numbers.push_back(cartnum);
+      c_good_titles.push_back(title);
+    }
+  }
+  delete group;
+}
+
+
+void Journal::addFailure(const QString &groupname,QString filename,
+			 const QString &err_msg)
+{
+  QString errors;
+  QString subject;
+  QString body;
+  RDGroup *group=new RDGroup(groupname);
+  QStringList addrs=
+    group->notifyEmailAddress().split(",",QString::SkipEmptyParts);
+
+  if(addrs.size()>0) {
+    filename=filename.split("/",QString::SkipEmptyParts).last();
+    if(c_send_immediately) {
+      subject=QObject::tr("Rivendell import FAILURE for file")+": "+filename;
+      body+=QObject::tr("Rivendell File Import Report")+"\n";
+      body+="\n";
+      body+=QObject::tr("IMPORT FAILED!")+"\n";
+      body+="\n";
+      body+=QObject::tr("Filename")+": "+filename+"\n";
+      body+=QObject::tr("Submitted by")+": ";
+      if(rda->user()->emailAddress().isEmpty()) {
+	body+=rda->user()->name()+"\n";
+      }
+      else {
+	body+=rda->user()->emailAddress()+"\n";
+      }
+      body+=QObject::tr("Group")+": "+groupname+"\n";
+      body+=QObject::tr("Reason")+": "+err_msg+"\n";
+
+      if(!RDSendMail(&errors,subject,body,rda->system()->originEmailAddress(),
+		     addrs)) {
+	rda->syslog(LOG_WARNING,"email send failed [%s]",
+		    errors.toUtf8().constData());
+      }
+    }
+    else {
+      c_bad_groups.push_back(groupname);
+      c_bad_filenames.push_back(filename);
+      c_bad_errors.push_back(err_msg);
+    }
+  }
+
+  delete group;
+}
+
+
+void Journal::sendAll()
+{
+  QStringList used_addrs;
+
+  //
+  // Successful imports
+  //
+  used_addrs.clear();
+  if(c_good_groups.size()>0) {
+    QMultiMap<QString,QString> grp_map=GroupsByAddress(c_good_groups);
+    for(QMap<QString,QString>::const_iterator it=grp_map.begin();
+	it!=grp_map.end();it++) {
+      if(!used_addrs.contains(it.key())) {
+	QString errors;
+	QString subject;
+	QString body;
+	QString from_addr;
+	QStringList to_addrs;
+
+	from_addr=rda->system()->originEmailAddress();
+	to_addrs=it.key().split(",",QString::SkipEmptyParts);
+	subject=QObject::tr("Rivendell import report")+"\n";
+
+	body+=QObject::tr("Rivendell File Import Report")+"\n";
+	body+="\n";
+	body+=QObject::tr("-Group---- -Cart- -Title------------------------ -Filename-------------")+"\n";
+	QStringList grps=grp_map.values(it.key());
+	for(int i=0;i<grps.size();i++) {
+	  for(int j=0;j<c_good_groups.size();j++) {
+	    if(c_good_groups.at(j)==grps.at(i)) {
+	      body+=QString().sprintf("%-10s %06u %-30s %-22s\n",
+			grps.at(i).left(10).toUtf8().constData(),
+			c_good_cart_numbers.at(j),
+			c_good_titles.at(j).left(30).toUtf8().constData(),
+			c_good_filenames.at(j).left(22).toUtf8().constData());
+	    }
+	  }
+	}
+	if(!RDSendMail(&errors,subject,body,rda->system()->originEmailAddress(),
+		       to_addrs)) {
+	  rda->syslog(LOG_WARNING,"email send failed [%s]",
+		      errors.toUtf8().constData());
+	}
+	used_addrs.push_back(it.key());
+      }
+    }
+  }
+
+  //
+  // Failed imports
+  //
+  used_addrs.clear();
+  if(c_bad_groups.size()>0) {
+    QMultiMap<QString,QString> grp_map=GroupsByAddress(c_bad_groups);
+    for(QMap<QString,QString>::const_iterator it=grp_map.begin();
+	it!=grp_map.end();it++) {
+      if(!used_addrs.contains(it.key())) {
+	QString errors;
+	QString subject;
+	QString body;
+	QString from_addr;
+	QStringList to_addrs;
+
+	from_addr=rda->system()->originEmailAddress();
+	to_addrs=it.key().split(",",QString::SkipEmptyParts);
+	subject=QObject::tr("Rivendell import FAILURE report")+"\n";
+
+	body+=QObject::tr("Rivendell File Import FAILURE Report")+"\n";
+	body+="\n";
+	body+=QObject::tr("-Group---- -Reason------------------------------ -Filename-------------")+"\n";
+	QStringList grps=grp_map.values(it.key());
+	for(int i=0;i<grps.size();i++) {
+	  for(int j=0;j<c_bad_groups.size();j++) {
+	    if(c_bad_groups.at(j)==grps.at(i)) {
+	      body+=QString().sprintf("%-10s %-37s %-22s\n",
+			grps.at(i).left(10).toUtf8().constData(),
+			c_bad_errors.at(j).left(37).toUtf8().constData(),
+			c_bad_filenames.at(j).left(22).toUtf8().constData());
+	    }
+	  }
+	}
+	if(!RDSendMail(&errors,subject,body,rda->system()->originEmailAddress(),
+		       to_addrs)) {
+	  rda->syslog(LOG_WARNING,"email send failed [%s]",
+		      errors.toUtf8().constData());
+	}
+	used_addrs.push_back(it.key());
+      }
+    }
+  }
+}
+
+
+QMultiMap<QString,QString> Journal::GroupsByAddress(QStringList groups) const
+{
+  RDGroup *grp;
+  QMultiMap<QString,QString> ret;
+
+  for(int i=0;i<groups.size();i++) {
+    grp=new RDGroup(groups.at(i));
+    if(!grp->notifyEmailAddress().isEmpty()) {
+      if(!ret.contains(grp->notifyEmailAddress(),grp->name())) {
+	ret.insert(grp->notifyEmailAddress(),grp->name());
+      }
+    }
+    delete grp;
+  }
+
+  return ret;
+}
diff --git a/utils/rdimport/journal.h b/utils/rdimport/journal.h
new file mode 100644
index 00000000..dab59767
--- /dev/null
+++ b/utils/rdimport/journal.h
@@ -0,0 +1,52 @@
+// journal.h
+//
+// E-mail file importation actions
+//
+//   (C) Copyright 2020 Fred Gleason <fredg@paravelsystems.com>
+//
+//   This program is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU General Public License version 2 as
+//   published by the Free Software Foundation.
+//
+//   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., 675 Mass Ave, Cambridge, MA 02139, USA.
+//
+
+#ifndef JOURNAL_H
+#define JOURNAL_H
+
+#include <QList>
+#include <QMultiMap>
+#include <QString>
+#include <QStringList>
+
+class Journal
+{
+ public:
+  Journal(bool send_immediately);
+  void addSuccess(const QString &groupname,QString filename,
+		  unsigned cartnum,const QString &title);
+  void addFailure(const QString &groupname,QString filename,
+		  const QString &err_msg);
+  void sendAll();
+
+ private:
+  QMultiMap<QString,QString> GroupsByAddress(QStringList groups) const;
+  bool c_send_immediately;
+  QStringList c_good_groups;
+  QStringList c_good_filenames;
+  QList<unsigned> c_good_cart_numbers;
+  QStringList c_good_titles;
+  QStringList c_bad_groups;
+  QStringList c_bad_filenames;
+  QStringList c_bad_errors;
+};
+
+
+#endif  // JOURNAL_H
diff --git a/utils/rdimport/rdimport.cpp b/utils/rdimport/rdimport.cpp
index 7baf9629..5b067ab5 100644
--- a/utils/rdimport/rdimport.cpp
+++ b/utils/rdimport/rdimport.cpp
@@ -99,6 +99,8 @@ MainObject::MainObject(QObject *parent)
   import_xml=false;
   import_to_mono=false;
   import_failed_imports=0;
+  import_send_mail=false;
+  import_mail_per_file=false;
 
   //
   // Open the Database
@@ -445,6 +447,15 @@ MainObject::MainObject(QObject *parent)
       import_xml=true;
       rda->cmdSwitch()->setProcessed(i,true);
     }
+    if(rda->cmdSwitch()->key(i)=="--send-mail") {
+      import_send_mail=true;
+      rda->cmdSwitch()->setProcessed(i,true);
+    }
+    if(rda->cmdSwitch()->key(i)=="--mail-per-file") {
+      import_mail_per_file=true;
+      import_send_mail=true;
+      rda->cmdSwitch()->setProcessed(i,true);
+    }
   }
 
   //
@@ -707,6 +718,17 @@ MainObject::MainObject(QObject *parent)
   else {
     Log(LOG_INFO,QString(" Broken format workarounds are DISABLED\n"));
   }
+  if(import_send_mail) {
+    if(import_mail_per_file) {
+      Log(LOG_INFO,QString(" E-mail report per file is ENABLED\n"));
+    }
+    else {
+      Log(LOG_INFO,QString(" Summary e-mail report is ENABLED\n"));
+    }
+  }
+  else {
+    Log(LOG_INFO,QString(" E-mail reporting is DISABLED\n"));
+  }
   if(import_create_dates) {
     Log(LOG_INFO,QString(" Import Create Dates mode is ON\n"));
     Log(LOG_INFO,QString().sprintf(" Import Create Start Date Offset = %d days\n",import_create_startdate_offset));
@@ -800,6 +822,11 @@ MainObject::MainObject(QObject *parent)
     }
   }
 
+  //
+  // Start the email journal
+  //
+  import_journal=new Journal(import_mail_per_file);
+
   // 
   // Setup Signal Handling 
   //
@@ -968,18 +995,19 @@ MainObject::Result MainObject::ImportFile(const QString &filename,
 	Log(LOG_WARNING,QString().sprintf(
 		" File \"%s\" is not readable or not a recognized format, skipping...\n",
 		(const char *)RDGetBasePart(filename).toUtf8()));
-	delete wavefile;
-	delete wavedata;
-	delete effective_group;
 	if(!import_run) {
 	  NormalExit();
 	}
 	if(!import_temp_fix_filename.isEmpty()) {
-//	  printf("Fixed Name: %s\n",(const char *)import_temp_fix_filename);
 	  QFile::remove(import_temp_fix_filename);
 	  import_temp_fix_filename="";
 	}
 	import_failed_imports++;
+	import_journal->addFailure(effective_group->name(),filename,
+				   tr("unknown/unrecognized file format"));
+	delete wavefile;
+	delete wavedata;
+	delete effective_group;
 	return MainObject::FileBad;
       }
       Log(LOG_WARNING,QString().sprintf("success.\n"));
@@ -989,9 +1017,6 @@ MainObject::Result MainObject::ImportFile(const QString &filename,
       Log(LOG_WARNING,QString().sprintf(
         " File \"%s\" is not readable or not a recognized format, skipping...\n",
         (const char *)RDGetBasePart(filename).toUtf8()));
-      delete wavefile;
-      delete wavedata;
-      delete effective_group;
       if(!import_run) {
 	NormalExit();
       }
@@ -1000,6 +1025,11 @@ MainObject::Result MainObject::ImportFile(const QString &filename,
 	import_temp_fix_filename="";
       }
       import_failed_imports++;
+      import_journal->addFailure(effective_group->name(),filename,
+				 tr("unknown/unrecognized file format"));
+      delete wavefile;
+      delete wavedata;
+      delete effective_group;
       return MainObject::FileBad;
     }
   }
@@ -1042,10 +1072,12 @@ MainObject::Result MainObject::ImportFile(const QString &filename,
 	      " File \"%s\" has an invalid or out of range Cart Number, skipping...\n",
 	      (const char *)RDGetBasePart(filename).toUtf8()));
       wavefile->closeWave();
+      import_failed_imports++;
+      import_journal->addFailure(effective_group->name(),filename,
+				 tr("invalid/out-of-range cart number"));
       delete wavefile;
       delete wavedata;
       delete effective_group;
-      import_failed_imports++;
       return MainObject::FileBad;
     }
   }
@@ -1056,9 +1088,6 @@ MainObject::Result MainObject::ImportFile(const QString &filename,
     Log(LOG_ERR,QString().sprintf("rdimport: no free carts available in specified group\n"));
     wavefile->closeWave();
     import_failed_imports++;
-    delete wavefile;
-    delete wavedata;
-    delete effective_group;
     import_failed_imports++;
     if(import_drop_box) {
       if(!import_run) {
@@ -1068,6 +1097,11 @@ MainObject::Result MainObject::ImportFile(const QString &filename,
 	QFile::remove(import_temp_fix_filename);
 	import_temp_fix_filename="";
       }
+      import_journal->addFailure(effective_group->name(),filename,
+				 tr("no free cart available in group"));
+      delete wavefile;
+      delete wavedata;
+      delete effective_group;
       return MainObject::NoCart;
     }
     exit(RDApplication::ExitImportFailed);
@@ -1087,8 +1121,10 @@ MainObject::Result MainObject::ImportFile(const QString &filename,
     cart->addCut(import_format,import_bitrate,import_channels);
   if(cutnum<0) {
     Log(LOG_WARNING,QString().sprintf("rdimport: no free cuts available in cart %06u\n",*cartnum));
-    delete cart;
     import_failed_imports++;
+    import_journal->addFailure(effective_group->name(),filename,
+			       tr("no free cut available in cart"));
+    delete cart;
     return MainObject::NoCut;
   }
   RDCut *cut=new RDCut(*cartnum,cutnum);
@@ -1149,9 +1185,6 @@ MainObject::Result MainObject::ImportFile(const QString &filename,
     delete cut;
     delete cart;
     wavefile->closeWave();
-    delete wavefile;
-    delete wavedata;
-    delete effective_group;
     if(!import_run) {
       NormalExit();
     }
@@ -1160,6 +1193,11 @@ MainObject::Result MainObject::ImportFile(const QString &filename,
       import_temp_fix_filename="";
     }
     import_failed_imports++;
+    import_journal->addFailure(effective_group->name(),filename,
+			       tr("corrupt audio file"));
+    delete wavefile;
+    delete wavedata;
+    delete effective_group;
     return MainObject::FileBad;
     break;
   }
@@ -1351,6 +1389,9 @@ MainObject::Result MainObject::ImportFile(const QString &filename,
     delete ll;
   }
 
+  import_journal->
+    addSuccess(effective_group->name(),filename,*cartnum,cart->title());
+
   delete settings;
   delete conv;
   delete cut;
@@ -2176,6 +2217,7 @@ void MainObject::Log(int prio,const QString &msg) const
 
 void MainObject::NormalExit() const
 {
+  import_journal->sendAll();
   if(import_failed_imports>0) {
     exit(RDApplication::ExitImportFailed);
   }
diff --git a/utils/rdimport/rdimport.h b/utils/rdimport/rdimport.h
index 72e4a520..c9ea489a 100644
--- a/utils/rdimport/rdimport.h
+++ b/utils/rdimport/rdimport.h
@@ -28,6 +28,8 @@
 #include <qsqldatabase.h>
 #include <qfileinfo.h>
 #include <qdatetime.h>
+#include <QList>
+#include <QStringList>
 
 #include <rdcart.h>
 #include <rdcut.h>
@@ -36,6 +38,7 @@
 #include <rdwavedata.h>
 #include <rdwavefile.h>
 
+#include "journal.h"
 #include "markerset.h"
 
 #define RDIMPORT_TEMP_BASENAME "rdimp"
@@ -113,6 +116,8 @@ class MainObject : public QObject
   int import_autotrim_level;
   int import_segue_level;
   int import_segue_length;
+  bool import_send_mail;
+  bool import_mail_per_file;
   unsigned import_cart_number;
   QString import_metadata_pattern;
   QString import_output_pattern;
@@ -151,6 +156,7 @@ class MainObject : public QObject
   MarkerSet *import_segue_markers;
   MarkerSet *import_fadedown_marker;
   MarkerSet *import_fadeup_marker;
+  Journal *import_journal;
 };