Add signature info to verify category
authorAndre Heinecke <aheinecke@intevation.de>
Tue, 16 Oct 2018 08:24:05 +0000 (10:24 +0200)
committerAndre Heinecke <aheinecke@intevation.de>
Tue, 16 Oct 2018 08:32:13 +0000 (10:32 +0200)
* src/categorymanager.cpp, src/categorymanager.h: New.
* src/Makefile.am: Add it.
* src/cpphelp.cpp, src/cpphelp.h (join): New.
* src/gpgoladdin.cpp, src/gpgoladdin.h
(Gpgoladdin::get_category_mngr): New.
* src/mail.cpp (Mail::~Mail): Remove categories.
(Mail::updateCategories_o): Use new code for categories.
(Mail::storeID): New carry store info.
* src/oomhelp.cpp (create_category): Fix dbg.
(get_store_for_id): New helper.
(ensure_category_exists): Remove need for appl.
(remove_category): Rewrite to allow partial matches.
(_delete_category): New helper.
(delete_category): New. Delete a category.
(delete_all_categories_starting_with): New.
* src/oomhelp.h: Update accordingly.

--
On startup we delete all old categories to cleanup e.g. after
we crashed and a category was left over. The categories
are only created on the stores where they are required and
only temporary as long as the mail is loaded.

As several mails can share the same category we need the
categorymanager to do some ref counting and help a bit.

GnuPG-Bug-Id: T4183

src/Makefile.am
src/categorymanager.cpp [new file with mode: 0644]
src/categorymanager.h [new file with mode: 0644]
src/cpphelp.cpp
src/cpphelp.h
src/gpgoladdin.cpp
src/gpgoladdin.h
src/mail.cpp
src/mail.h
src/oomhelp.cpp
src/oomhelp.h

index 0993bd5..369ce6d 100644 (file)
@@ -31,6 +31,7 @@ gpgol_SOURCES = \
     addressbook.cpp addressbook.h \
     application-events.cpp \
     attachment.h attachment.cpp \
+    categorymanager.h categorymanager.cpp \
     common.h common.cpp \
     common_indep.h common_indep.c \
     cpphelp.cpp cpphelp.h \
diff --git a/src/categorymanager.cpp b/src/categorymanager.cpp
new file mode 100644 (file)
index 0000000..1716b52
--- /dev/null
@@ -0,0 +1,250 @@
+/* @file categorymanager.cpp
+ * @brief Handles category management
+ *
+ * Copyright (C) 2018 Intevation GmbH
+ *
+ * This file is part of GpgOL.
+ *
+ * GpgOL is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GpgOL 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "categorymanager.h"
+#include "common.h"
+#include "mail.h"
+#include "gpgoladdin.h"
+#include "oomhelp.h"
+
+#include <unordered_map>
+
+class CategoryManager::Private
+{
+public:
+  Private()
+  {
+  }
+
+  void createCategory (shared_disp_t store,
+                       const std::string &category, int color)
+    {
+      TSTART;
+      LPDISPATCH categories = get_oom_object (store.get(), "Categories");
+      if (!categories)
+        {
+          STRANGEPOINT;
+          TRETURN;
+        }
+      if (create_category (categories, category.c_str (), color))
+        {
+          log_debug ("%s:%s: Failed to create category %s",
+                     SRCNAME, __func__, anonstr (category.c_str()));
+          TRETURN;
+        }
+      TRETURN;
+    }
+
+  void registerCategory (const std::string &storeID,
+                         const std::string &category)
+    {
+      TSTART;
+      auto storeIt = mCategoryStoreMap.find (storeID);
+      if (storeIt == mCategoryStoreMap.end())
+        {
+          /* First category for this store. Create a new
+             category ref map. */
+          std::unordered_map <std::string, int> categoryMap;
+          categoryMap.insert (std::make_pair (category, 1));
+          mCategoryStoreMap.insert (std::make_pair (storeID, categoryMap));
+          log_debug ("%s:%s: Register category %s in new store %s ref now 1",
+                     SRCNAME, __func__, anonstr (category.c_str()),
+                     anonstr (storeID.c_str()));
+          TRETURN;
+        }
+      auto categoryIt = storeIt->second.find (category);
+      if (categoryIt == storeIt->second.end ())
+        {
+          storeIt->second.insert (std::make_pair (category, 1));
+          log_debug ("%s:%s: Register category %s in store %s ref now 1",
+                     SRCNAME, __func__, anonstr (category.c_str()),
+                     anonstr (storeID.c_str()));
+        }
+      else
+        {
+          categoryIt->second++;
+          log_debug ("%s:%s: Register category %s in store %s ref now %i",
+                     SRCNAME, __func__, anonstr (category.c_str()),
+                     anonstr (storeID.c_str()), categoryIt->second);
+        }
+      TRETURN;
+    }
+
+  void unregisterCategory (const std::string &storeID,
+                           const std::string &category)
+    {
+      TSTART;
+      auto storeIt = mCategoryStoreMap.find (storeID);
+      if (storeIt == mCategoryStoreMap.end ())
+        {
+          log_error ("%s:%s: Unregister called for unregistered store %s",
+                     SRCNAME, __func__, anonstr (storeID.c_str()));
+          TRETURN;
+        }
+      auto categoryIt = storeIt->second.find (category);
+      if (categoryIt == storeIt->second.end ())
+        {
+          log_debug ("%s:%s: Unregister %s not found for store %s",
+                     SRCNAME, __func__, anonstr (category.c_str()),
+                     anonstr (storeID.c_str()));
+          TRETURN;
+        }
+      categoryIt->second--;
+      log_debug ("%s:%s: Unregister category %s in store %s ref now %i",
+                 SRCNAME, __func__, anonstr (category.c_str()),
+                 anonstr (storeID.c_str()), categoryIt->second);
+      if (categoryIt->second < 0)
+        {
+          log_debug ("%s:%s: Unregister %s negative for store %s",
+                     SRCNAME, __func__, anonstr (category.c_str()),
+                     anonstr (storeID.c_str()));
+          TRETURN;
+        }
+      if (categoryIt->second == 0)
+        {
+          log_debug ("%s:%s: Deleting %s for store %s",
+                     SRCNAME, __func__, anonstr (category.c_str()),
+                     anonstr (storeID.c_str()));
+
+          LPDISPATCH store = get_store_for_id (storeID.c_str());
+          if (!store)
+            {
+              STRANGEPOINT;
+              TRETURN;
+            }
+          delete_category (store, category.c_str ());
+          storeIt->second.erase (categoryIt);
+        }
+      TRETURN;
+    }
+
+  bool categoryExistsInMap (const std::string &storeID,
+                            const std::string &category)
+    {
+      const auto it = mCategoryStoreMap.find (storeID);
+      if (it == mCategoryStoreMap.end ())
+        {
+          return false;
+        }
+      return it->second.find (category) != it->second.end();
+    }
+
+private:
+  /* Map from: store to map of category -> refs. */
+  std::unordered_map<std::string,
+    std::unordered_map<std::string, int> > mCategoryStoreMap;
+};
+
+/* static */
+std::shared_ptr <CategoryManager>
+CategoryManager::instance ()
+{
+  return GpgolAddin::get_instance ()->get_category_mngr ();
+}
+
+CategoryManager::CategoryManager():
+  d(new Private)
+{
+}
+
+std::string
+CategoryManager::addCategoryToMail (Mail *mail, const std::string &category, int color)
+{
+  TSTART;
+  std::string ret;
+  if (!mail || category.empty())
+    {
+      TRETURN ret;
+    }
+
+  auto store = MAKE_SHARED (get_oom_object (mail->item (), "Parent.Store"));
+  if (!store)
+    {
+      log_error ("%s:%s Failed to obtain store",
+                 SRCNAME, __func__);
+      TRETURN std::string ();
+    }
+  char *storeID = get_oom_string (store.get (), "StoreID");
+  if (!storeID)
+    {
+      log_error ("%s:%s Failed to obtain storeID",
+                 SRCNAME, __func__);
+      TRETURN std::string ();
+    }
+  ret = storeID;
+  xfree (storeID);
+
+  if (!d->categoryExistsInMap (ret, category))
+    {
+      d->createCategory (store, category, color);
+    }
+  d->registerCategory (ret, category);
+
+  if (add_category (mail->item (), category.c_str()))
+    {
+      /* Probably the category already existed there
+         so it is not super worrysome. */
+      log_debug ("%s:%s Failed to add category.",
+                 SRCNAME, __func__);
+    }
+  return ret;
+}
+
+void
+CategoryManager::removeCategory (Mail *mail, const std::string &category)
+{
+  TSTART;
+  if (!mail || category.empty())
+    {
+      STRANGEPOINT;
+      TRETURN;
+    }
+  if (remove_category (mail->item (), category.c_str (), true))
+    {
+      log_debug ("%s:%s Failed to remvoe category.",
+                 SRCNAME, __func__);
+    }
+  d->unregisterCategory (mail->storeID (), category.c_str ());
+
+  TRETURN;
+}
+
+/* static */
+void
+CategoryManager::removeAllGpgOLCategories ()
+{
+  TSTART;
+  delete_all_categories_starting_with ("GpgOL: ");
+  TRETURN;
+}
+
+/* static */
+const std::string &
+CategoryManager::getEncMailCategory ()
+{
+  static std::string decStr;
+  if (decStr.empty())
+    {
+      decStr = std::string ("GpgOL: ") +
+                            std::string (_("Encrypted Message"));
+    }
+  return decStr;
+}
diff --git a/src/categorymanager.h b/src/categorymanager.h
new file mode 100644 (file)
index 0000000..0e96485
--- /dev/null
@@ -0,0 +1,69 @@
+#ifndef CATEGORYMANAGER_H
+#define CATEGORYMANAGER_H
+
+/* @file categorymanager.h
+ * @brief Handles category management
+ *
+ * Copyright (C) 2018 Intevation GmbH
+ *
+ * This file is part of GpgOL.
+ *
+ * GpgOL is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GpgOL 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <memory>
+#include <string>
+
+class Mail;
+class GpgolAddin;
+
+/* The category manager is supposed to be only accessed from
+   the main thread and is not guarded by locks. */
+class CategoryManager
+{
+    friend class GpgolAddin;
+
+protected:
+    /** Internal ctor */
+    explicit CategoryManager ();
+
+public:
+    /** Get the CategoryManager */
+    static std::shared_ptr<CategoryManager> instance ();
+
+    /** Add a category to a mail.
+
+      @returns the storeID of the mail / category.
+    */
+    std::string addCategoryToMail (Mail *mail, const std::string &category,
+                                   int color);
+
+    /** Remove the category @category */
+    void removeCategory (Mail * mail,
+                         const std::string &category);
+
+    /** Remove all GpgOL categories from all stores. */
+    static void removeAllGpgOLCategories ();
+
+    /** Get the name of the encryption category. */
+    static const std::string & getEncMailCategory ();
+
+private:
+    class Private;
+    std::shared_ptr<Private> d;
+};
+
+#endif
index 6ed5ad0..30daa9e 100644 (file)
@@ -72,6 +72,20 @@ trim(std::string &s)
   rtrim (s);
 }
 
+void
+join(const std::vector<std::string>& v, const char *c, std::string& s)
+{
+  s.clear();
+  for (auto p = v.begin(); p != v.end(); ++p)
+    {
+      s += *p;
+      if (p != v.end() - 1)
+        {
+          s += c;
+        }
+    }
+}
+
 char **
 vector_to_cArray(const std::vector<std::string> &vec)
 {
index 0b905b1..cd56349 100644 (file)
@@ -38,6 +38,9 @@ void rtrim (std::string &s);
 void ltrim (std::string &s);
 void trim (std::string &s);
 
+/* Join a string vector */
+void join(const std::vector<std::string>& v, const char *c, std::string& s);
+
 /* Convert a string vector to a null terminated char array */
 char **vector_to_cArray (const std::vector<std::string> &vec);
 std::vector <std::string> cArray_to_vector (const char **cArray);
index acc1969..aca9943 100644 (file)
@@ -48,6 +48,7 @@
 #include "addin-options.h"
 #include "cpphelp.h"
 #include "dispcache.h"
+#include "categorymanager.h"
 
 #include <gpg-error.h>
 #include <list>
@@ -511,11 +512,11 @@ GpgolAddin::OnStartupComplete (SAFEARRAY** custom)
                  SRCNAME, __func__);
     }
 
-  /* Set up categories */
-  const char *decCategory = _("GpgOL: Encrypted Message");
-  const char *verifyCategory = _("GpgOL: Trusted Sender Address");
-  ensure_category_exists (m_application, decCategory, 8);
-  ensure_category_exists (m_application, verifyCategory, 5);
+  /* Clean GpgOL prefixed categories.
+     They might be left over from a crash or something unexpected
+     error. We want to avoid pollution with the signed by categories.
+  */
+  CategoryManager::removeAllGpgOLCategories ();
   install_forms ();
   m_applicationEventSink = install_ApplicationEvents_sink (m_application);
   m_explorersEventSink = install_explorer_sinks (m_application);
@@ -1251,3 +1252,14 @@ GpgolAddin::unregisterExplorerSink (LPDISPATCH sink)
   log_error ("%s:%s: Unregister %p which was not registered.",
              SRCNAME, __func__, sink);
 }
+
+std::shared_ptr <CategoryManager>
+GpgolAddin::get_category_mngr ()
+{
+  if (!m_category_mngr)
+    {
+      m_category_mngr = std::shared_ptr<CategoryManager> (
+                                                  new CategoryManager ());
+    }
+  return m_category_mngr;
+}
index 3627a01..0bd6f78 100644 (file)
@@ -32,6 +32,7 @@
 class GpgolAddinRibbonExt;
 class ApplicationEventListener;
 class DispCache;
+class CategoryManager;
 
 /* Enums for the IDTExtensibility2 interface*/
 typedef enum
@@ -223,6 +224,8 @@ public:
   /* Invalidate the ribbons. */
   void invalidateRibbons ();
 
+  std::shared_ptr<CategoryManager> get_category_mngr ();
+
 private:
   ULONG m_lRef;
   GpgolRibbonExtender* m_ribbonExtender;
@@ -238,6 +241,7 @@ private:
   std::vector<LPDISPATCH> m_explorerEventSinks;
   std::shared_ptr<DispCache> m_dispcache;
   std::vector<LPDISPATCH> m_ribbon_uis;
+  std::shared_ptr<CategoryManager> m_category_mngr;
 };
 
 class GpgolAddinFactory: public IClassFactory
index bfff980..134ea9d 100644 (file)
@@ -21,6 +21,8 @@
  */
 
 #include "config.h"
+
+#include "categorymanager.h"
 #include "dialogs.h"
 #include "common.h"
 #include "mail.h"
@@ -50,6 +52,7 @@
 #include <gpg-error.h>
 
 #include <map>
+#include <unordered_map>
 #include <set>
 #include <vector>
 #include <memory>
@@ -192,6 +195,10 @@ Mail::~Mail()
       gpgol_unlock (&uid_map_lock);
     }
 
+  log_oom ("%s:%s: removing categories",
+                 SRCNAME, __func__);
+  removeCategories_o ();
+
   log_oom ("%s:%s: releasing mailitem",
                  SRCNAME, __func__);
   gpgol_release(m_mailitem);
@@ -2133,7 +2140,8 @@ Mail::updateSigstate ()
                          anonstr (sender.c_str ()));
             }
         }
-
+      /* Sigsum valid or green is somehow not set in this case.
+       * Which is strange as AFAIK this worked in the past. */
       if ((sig.summary() & Signature::Summary::Valid) &&
           m_uid.origin() == GpgME::Key::OriginWKD &&
           (sig.validity() == Signature::Validity::Unknown ||
@@ -2190,10 +2198,20 @@ void
 Mail::removeCategories_o ()
 {
   TSTART;
-  const char *decCategory = _("GpgOL: Encrypted Message");
-  const char *verifyCategory = _("GpgOL: Trusted Sender Address");
-  remove_category (m_mailitem, decCategory);
-  remove_category (m_mailitem, verifyCategory);
+  if (!m_store_id.empty () && !m_verify_category.empty ())
+    {
+      log_oom ("%s:%s: Unreffing verify category",
+                       SRCNAME, __func__);
+      CategoryManager::instance ()->removeCategory (this,
+                                                    m_verify_category);
+    }
+  if (!m_store_id.empty () && !m_decrypt_result.isNull())
+    {
+      log_oom ("%s:%s: Unreffing dec category",
+                       SRCNAME, __func__);
+      CategoryManager::instance ()->removeCategory (this,
+                                CategoryManager::getEncMailCategory ());
+    }
   TRETURN;
 }
 
@@ -2261,30 +2279,116 @@ resize_active_window ()
   TRETURN;
 }
 
+#if 0
+static std::string
+pretty_id (const char *keyId)
+{
+  /* Three spaces, four quads and a NULL */
+  char buf[20];
+  buf[19] = '\0';
+  if (!keyId)
+    {
+      return std::string ("null");
+    }
+  size_t len = strlen (keyId);
+  if (!len)
+    {
+      return std::string ("empty");
+    }
+  if (len < 16)
+    {
+      return std::string (_("Invalid Key"));
+    }
+  const char *p = keyId + (len - 16);
+  int j = 0;
+  for (size_t i = 0; i < 16; i++)
+    {
+      if (i && i % 4 == 0)
+        {
+          buf[j++] = ' ';
+        }
+      buf[j++] = *(p + i);
+    }
+  return std::string (buf);
+}
+#endif
+
 void
 Mail::updateCategories_o ()
 {
   TSTART;
-  const char *decCategory = _("GpgOL: Encrypted Message");
-  const char *verifyCategory = _("GpgOL: Trusted Sender Address");
+
+  auto mngr = CategoryManager::instance ();
   if (isValidSig ())
     {
-      add_category (m_mailitem, verifyCategory);
+      char *buf;
+      /* Resolve to the primary fingerprint */
+#if 0
+      const auto sigKey = KeyCache::instance ()->getByFpr (m_sig.fingerprint (),
+                                                           true);
+      const char *sigFpr;
+      if (sigKey.isNull())
+        {
+          sigFpr = m_sig.fingerprint ();
+        }
+      else
+        {
+          sigFpr = sigKey.primaryFingerprint ();
+        }
+#endif
+      /* If m_uid addrSpec would not return a result we would never
+       * have gotten the UID. */
+      int lvl = get_signature_level ();
+      gpgrt_asprintf (&buf, "GpgOL: %s %i %s '%s'", _("Level"), lvl,
+                      _("trust in"),
+                      m_uid.addrSpec ().c_str ());
+      memdbg_alloc (buf);
+
+      int color = 0;
+      if (lvl == 2)
+        {
+          color = 7;
+        }
+      if (lvl == 3)
+        {
+          color = 5;
+        }
+      if (lvl == 4)
+        {
+          color = 20;
+        }
+      m_store_id = mngr->addCategoryToMail (this, buf, color);
+      m_verify_category = buf;
+      xfree (buf);
     }
   else
     {
-      remove_category (m_mailitem, verifyCategory);
+      remove_category (m_mailitem, "GpgOL: ", false);
     }
 
   if (!m_decrypt_result.isNull())
     {
-      add_category (m_mailitem, decCategory);
+      const auto id = mngr->addCategoryToMail (this,
+                                 CategoryManager::getEncMailCategory (),
+                                 8);
+      if (m_store_id.empty())
+        {
+          m_store_id = id;
+        }
+      if (m_store_id != id)
+        {
+          log_error ("%s:%s unexpected store mismatch "
+                     "between '%s' and dec cat '%s'",
+                     SRCNAME, __func__, m_store_id.c_str(), id.c_str());
+        }
     }
   else
     {
       /* As a small safeguard against fakes we remove our
          categories */
-      remove_category (m_mailitem, decCategory);
+      remove_category (m_mailitem,
+                       CategoryManager::getEncMailCategory ().c_str (),
+                       true);
     }
 
   resize_active_window();
index c4f2369..05d26d0 100644 (file)
@@ -598,6 +598,9 @@ public:
   /* Gets an additional reference for GetInspector.CurrentItem */
   void refCurrentItem ();
 
+  /* Get the storeID for this mail */
+  std::string storeID() { return m_store_id; }
+
 private:
   void updateCategories_o ();
   void updateSigstate ();
@@ -649,5 +652,7 @@ private:
   int m_locate_count; /* The number of key locates pending for this mail. */
   bool m_is_about_to_be_moved;
   bool m_locate_in_progress; /* Simplified state variable for locate */
+  std::string m_store_id; /* Store id for categories */
+  std::string m_verify_category; /* The category string for the verify result */
 };
 #endif // MAIL_H
index 7cc922a..1cc2d0b 100644 (file)
 #include <windows.h>
 #include <olectl.h>
 #include <string>
+#include <sstream>
+#include <algorithm>
 #include <rpc.h>
 
 #include "common.h"
 
 #include "oomhelp.h"
+#include "cpphelp.h"
 #include "gpgoladdin.h"
 
 HRESULT
@@ -1763,7 +1766,7 @@ get_oom_mapi_session ()
   TRETURN session;
 }
 
-static int
+int
 create_category (LPDISPATCH categories, const char *category, int color)
 {
   TSTART;
@@ -1838,21 +1841,66 @@ create_category (LPDISPATCH categories, const char *category, int color)
     }
   VariantClear (&rVariant);
   log_oom ("%s:%s: Created category '%s'",
-             SRCNAME, __func__, category);
+             SRCNAME, __func__, anonstr (category));
   TRETURN 0;
 }
 
+LPDISPATCH
+get_store_for_id (const char *storeID)
+{
+  TSTART;
+  LPDISPATCH application = GpgolAddin::get_instance ()->get_application ();
+  if (!application || !storeID)
+    {
+      TRACEPOINT;
+      TRETURN nullptr;
+    }
+
+  LPDISPATCH stores = get_oom_object (application, "Session.Stores");
+  if (!stores)
+    {
+      log_error ("%s:%s: No stores found.",
+                 SRCNAME, __func__);
+      TRETURN nullptr;
+    }
+  auto store_count = get_oom_int (stores, "Count");
+
+  for (int n = 1; n <= store_count; n++)
+    {
+      const auto store_str = std::string("Item(") + std::to_string(n) + ")";
+      LPDISPATCH store = get_oom_object (stores, store_str.c_str());
+
+      if (!store)
+        {
+          TRACEPOINT;
+          continue;
+        }
+      char *id = get_oom_string (store, "StoreID");
+      if (id && !strcmp (id, storeID))
+        {
+          gpgol_release (stores);
+          return store;
+        }
+      gpgol_release (store);
+    }
+  gpgol_release (stores);
+  return nullptr;
+}
+
 void
-ensure_category_exists (LPDISPATCH application, const char *category, int color)
+ensure_category_exists (const char *category, int color)
 {
   TSTART;
+  LPDISPATCH application = GpgolAddin::get_instance ()->get_application ();
   if (!application || !category)
     {
       TRACEPOINT;
       TRETURN;
     }
 
-  log_oom ("Ensure category exists called for %s, %i", category, color);
+  log_oom ("%s:%s: Ensure category exists called for %s, %i",
+           SRCNAME, __func__,
+           category, color);
 
   LPDISPATCH stores = get_oom_object (application, "Session.Stores");
   if (!stores)
@@ -1955,7 +2003,7 @@ add_category (LPDISPATCH mail, const char *category)
 }
 
 int
-remove_category (LPDISPATCH mail, const char *category)
+remove_category (LPDISPATCH mail, const char *category, bool exactMatch)
 {
   TSTART;
   char *tmp = get_oom_string (mail, "Categories");
@@ -1964,29 +2012,192 @@ remove_category (LPDISPATCH mail, const char *category)
       TRACEPOINT;
       TRETURN 1;
     }
-  std::string newstr (tmp);
+
+  std::vector<std::string> categories;
+  std::istringstream f(tmp);
+  std::string s;
+  while (std::getline(f, s, ','))
+    {
+      ltrim(s);
+      categories.push_back(s);
+    }
   xfree (tmp);
-  std::string cat (category);
 
-  size_t pos1 = newstr.find (cat);
-  size_t pos2 = newstr.find (std::string(", ") + cat);
-  if (pos1 == std::string::npos && pos2 == std::string::npos)
+  const std::string categoryStr = category;
+
+  categories.erase (std::remove_if (categories.begin(),
+                                    categories.end(),
+                                    [categoryStr, exactMatch] (const std::string &cat)
     {
-      log_oom ("%s:%s: category '%s' not found.",
-               SRCNAME, __func__, category);
-      TRETURN 0;
+      if (exactMatch)
+        {
+          return cat == categoryStr;
+        }
+      return cat.compare (0, categoryStr.size(), categoryStr) == 0;
+    }), categories.end ());
+  std::string newCategories;
+  join (categories, ", ", newCategories);
+
+  TRETURN put_oom_string (mail, "Categories", newCategories.c_str ());
+}
+
+static int
+_delete_category (LPDISPATCH categories, int idx)
+{
+  TSTART;
+  VARIANT aVariant[1];
+  DISPPARAMS dispparams;
+
+  dispparams.rgvarg = aVariant;
+  dispparams.rgvarg[0].vt = VT_INT;
+  dispparams.rgvarg[0].intVal = idx;
+  dispparams.cArgs = 1;
+  dispparams.cNamedArgs = 0;
+
+  TRETURN invoke_oom_method_with_parms (categories, "Remove", NULL,
+                                        &dispparams);
+}
+
+int
+delete_category (LPDISPATCH store, const char *category)
+{
+  TSTART;
+  if (!store || !category)
+    {
+      TRETURN -1;
     }
 
-  size_t len = cat.size();
-  if (pos2)
+  LPDISPATCH categories = get_oom_object (store, "Categories");
+  if (!categories)
     {
-      len += 2;
+      categories = get_oom_object (
+                      GpgolAddin::get_instance ()->get_application (),
+                      "Session.Categories");
+      if (!categories)
+        {
+          TRACEPOINT;
+          TRETURN -1;
+        }
     }
-  newstr.erase (pos2 != std::string::npos ? pos2 : pos1, len);
-  log_oom ("%s:%s: removing category '%s'",
-           SRCNAME, __func__, category);
 
-  TRETURN put_oom_string (mail, "Categories", newstr.c_str ());
+  auto count = get_oom_int (categories, "Count");
+  int ret = 0;
+  for (int i = 1; i <= count; i++)
+    {
+      const auto item_str = std::string("Item(") + std::to_string(i) + ")";
+      LPDISPATCH category_obj = get_oom_object (categories, item_str.c_str());
+      if (!category_obj)
+        {
+          TRACEPOINT;
+          gpgol_release (categories);
+          break;
+        }
+      char *name = get_oom_string (category_obj, "Name");
+      gpgol_release (category_obj);
+      if (name && !strcmp (category, name))
+        {
+          if ((ret = _delete_category (categories, i)))
+            {
+              log_error ("%s:%s: Failed to delete category '%s'",
+                         SRCNAME, __func__, anonstr (category));
+            }
+          else
+            {
+              log_debug ("%s:%s: Deleted category '%s'",
+                         SRCNAME, __func__, anonstr (category));
+            }
+          break;
+        }
+      xfree (name);
+    }
+
+  gpgol_release (categories);
+  TRETURN ret;
+}
+
+void
+delete_all_categories_starting_with (const char *string)
+{
+  LPDISPATCH application = GpgolAddin::get_instance ()->get_application ();
+  if (!application || !string)
+    {
+      TRACEPOINT;
+      TRETURN;
+    }
+
+  log_oom ("%s:%s: Delete categories starting with: \"%s\"",
+           SRCNAME, __func__, string);
+
+  LPDISPATCH stores = get_oom_object (application, "Session.Stores");
+  if (!stores)
+    {
+      log_error ("%s:%s: No stores found.",
+                 SRCNAME, __func__);
+      TRETURN;
+    }
+
+  auto store_count = get_oom_int (stores, "Count");
+
+  for (int n = 1; n <= store_count; n++)
+    {
+      const auto store_str = std::string("Item(") + std::to_string(n) + ")";
+      LPDISPATCH store = get_oom_object (stores, store_str.c_str());
+
+      if (!store)
+        {
+          TRACEPOINT;
+          continue;
+        }
+
+      LPDISPATCH categories = get_oom_object (store, "Categories");
+      if (!categories)
+        {
+          categories = get_oom_object (application, "Session.Categories");
+          if (!categories)
+            {
+              TRACEPOINT;
+              gpgol_release (store);
+              continue;
+            }
+        }
+
+      auto count = get_oom_int (categories, "Count");
+      std::vector<std::string> to_delete;
+      for (int i = 1; i <= count; i++)
+        {
+          const auto item_str = std::string("Item(") + std::to_string(i) + ")";
+          LPDISPATCH category_obj = get_oom_object (categories, item_str.c_str());
+          if (!category_obj)
+            {
+              TRACEPOINT;
+              gpgol_release (categories);
+              break;
+            }
+          char *name = get_oom_string (category_obj, "Name");
+          if (name && !strncmp (string, name, strlen (string)))
+            {
+              log_oom ("%s:%s: Found category for deletion '%s'",
+                       SRCNAME, __func__, anonstr(name));
+              to_delete.push_back (name);
+            }
+          /* We don't check the color here as the user may change that. */
+          gpgol_release (category_obj);
+          xfree (name);
+        }
+
+      /* Do this one after another to avoid messing with indexes. */
+      for (const auto &str: to_delete)
+        {
+          delete_category (store, str.c_str ());
+        }
+
+      gpgol_release (store);
+      /* Otherwise we have to create the category */
+      gpgol_release (categories);
+    }
+  gpgol_release (stores);
+  TRETURN;
+
 }
 
 static char *
index b922333..3f9199c 100644 (file)
@@ -333,14 +333,13 @@ invoke_oom_method_with_parms (LPDISPATCH pDisp, const char *name,
 LPMAPISESSION
 get_oom_mapi_session (void);
 
-/* Ensure a category of the name name exists in
-  the session for the Mail mail.
+/* Ensure a category of the name name exists.
 
   Creates the category with the specified color if required.
 
   returns 0 on success. */
 void
-ensure_category_exists (LPDISPATCH mail, const char *category, int color);
+ensure_category_exists (const char *category, int color);
 
 /* Add a category to a mail if it is not already added. */
 int
@@ -348,7 +347,21 @@ add_category (LPDISPATCH mail, const char *category);
 
 /* Remove a category from a mail if it was added. */
 int
-remove_category (LPDISPATCH mail, const char *category);
+remove_category (LPDISPATCH mail, const char *category, bool exactMatch);
+
+/* Create the category */
+int
+create_category (LPDISPATCH categories, const char *category, int color);
+
+/* Delete a category from the store. */
+int delete_category (LPDISPATCH store, const char *category);
+
+/* Delete categories starting with "string" from all stores. */
+void delete_all_categories_starting_with (const char *string);
+
+/* Iterate over application stores and return the one with the ID
+   @storeID */
+LPDISPATCH get_store_for_id (const char *storeID);
 
 /* Get a unique identifier for a mail object. The
    uuid is a custom property. If create is set