From 7d4e326e1e19a0d9c63a6ae3964b60e7da3602f4 Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Wed, 27 Aug 2025 14:56:16 -0600 Subject: [PATCH 01/27] Update CHANGELOG.md - Add comma-delimited CSV option - Add cc and bcc fields - Allow inline values for Recipient, cc, bcc --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9379d7..14cf4b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [Version 1.0.3A]() - Custom feature release 2025-08 + +- Add comma-delimited CSV option +- Add cc and bcc fields +- Allow inline values for Recipient, cc, bcc ## [Version 1.0.3](https://github.com/dataiku/dss-plugin-sendmail/releases/tag/v1.0.3) - Feature release - 2025-03 From 50a3199625ab7c06353c38fe372928590211dd7c Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Wed, 27 Aug 2025 14:56:57 -0600 Subject: [PATCH 02/27] Update plugin.json V1.0.3A --- plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin.json b/plugin.json index ee7b2ac..1b4d176 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "id": "sendmail", - "version": "1.0.3", + "version": "1.0.3A", "meta": { "label": "Send emails", "description": "Send emails based on a dataset containing a list of contacts, with optional attachments (other datasets)", From b6816683956767499b46caa04671b14c5debf6a1 Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Wed, 27 Aug 2025 15:08:28 -0600 Subject: [PATCH 03/27] Update recipe.json - Add comma-delimited CSV option - Add cc and bcc fields --- .../recipe.json | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/custom-recipes/send-mails-from-contacts-dataset/recipe.json b/custom-recipes/send-mails-from-contacts-dataset/recipe.json index e656acd..0a562d8 100644 --- a/custom-recipes/send-mails-from-contacts-dataset/recipe.json +++ b/custom-recipes/send-mails-from-contacts-dataset/recipe.json @@ -84,15 +84,7 @@ "type": "PASSWORD", "visibilityCondition" : "(model.mail_channel == null || model.mail_channel == '__DKU__DIRECT_SMTP__') && model.smtp_use_auth" }, - - { - "name": "recipient_column", - "label" : "Recipient (column)", - "type": "COLUMN", - "columnRole" : "contacts", - "description" : "Recipient of the email (from a column)", - "mandatory": true - }, + // 2025-08-27 Andy Holst added cc and bcc fields, and adjusted display order { "name": "sender_column", "label" : "Sender (column)", @@ -115,6 +107,28 @@ "type": "BOOLEAN", "visibilityCondition" : "!model.mail_channel.endsWith('__WITH_DEFINED_SENDER__')" }, + { + "name": "recipient_column", + "label" : "Recipient (column)", + "type": "COLUMN", + "columnRole" : "contacts", + "description" : "Recipient of the email (from a column)", + "mandatory": true + }, + { + "name": "cc_column", + "label" : "cc (column)", + "type": "COLUMN", + "columnRole" : "contacts", + "description" : "Courtesy copy (from a column)" + }, + { + "name": "bcc_column", + "label" : "bcc (column)", + "type": "COLUMN", + "columnRole" : "contacts", + "description" : "Blind courtesy copy (from a column)" + }, { "name": "subject_column", "label" : "Subject (column)", @@ -194,7 +208,7 @@ "label": "Attachments", "type": "SEPARATOR" }, - + // 2025-08-27 Andy Holst added CSV comma-delimited { "name": "attachment_type", "label" : "Attachments format", @@ -202,7 +216,8 @@ "selectChoices" : [ {"value": "send_no_attachments", "label": "Do not send attachments"}, {"value": "excel", "label": "Excel"}, - {"value": "csv", "label": "CSV"} + {"value": "csv", "label": "CSV tab-delimited"}, + {"value": "csv_comma", "label": "CSV comma-delimited"} ], "defaultValue" : "send_no_attachments", "description" : "File format for attachments" From 4d92fd5c52cd203007027e2a0f4988a51175cd52 Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Wed, 27 Aug 2025 15:10:21 -0600 Subject: [PATCH 04/27] Update CHANGELOG.md Remove note for feature that was not added. --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14cf4b5..4f269bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,6 @@ - Add comma-delimited CSV option - Add cc and bcc fields -- Allow inline values for Recipient, cc, bcc ## [Version 1.0.3](https://github.com/dataiku/dss-plugin-sendmail/releases/tag/v1.0.3) - Feature release - 2025-03 From bfc6fa30184554c09fec3d2aef184516ab9736c6 Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Wed, 27 Aug 2025 15:22:26 -0600 Subject: [PATCH 05/27] Update recipe.py - Add comma-delimited CSV option - Add cc and bcc fields --- .../recipe.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/custom-recipes/send-mails-from-contacts-dataset/recipe.py b/custom-recipes/send-mails-from-contacts-dataset/recipe.py index 0d54a97..43707b5 100644 --- a/custom-recipes/send-mails-from-contacts-dataset/recipe.py +++ b/custom-recipes/send-mails-from-contacts-dataset/recipe.py @@ -59,6 +59,10 @@ def parse_recipients(recipients): recipient_column = config.get('recipient_column', None) +# 2025-08-27 Andy Holst added cc and bcc fields +cc_column = config.get('cc_column', None) +bcc_column = config.get('bcc_column', None) + sender_column = config.get('sender_column', None) sender_value = config.get('sender_value', None) use_sender_value = config.get('use_sender_value', False) @@ -165,6 +169,18 @@ def parse_recipients(recipients): else: logging.info("No recipient for row - emailing will fail - row data: %s" % contact) contact_dict = dict(contact) + + # 2025-08-27 Andy Holst added cc and bcc fields + if cc_column: + cc_recipient = contact[cc_column] if contact[cc_column] != "" else None + else: + cc_recipient = None + + if bcc_column: + bcc_recipient = contact[bcc_column] if contact[bcc_column] != "" else None + else: + bcc_recipient = None + try: email_subject = build_email_subject(use_subject_value, subject_template, subject_column, contact_dict) email_body_text = build_email_message_text(use_body_value, body_template, attachments_templating_dict, contact_dict, body_column, @@ -172,7 +188,9 @@ def parse_recipients(recipients): recipients = parse_recipients(recipients_string) # Note - if the channel has a sender configured, the sender value will be ignored by the email client here sender = sender_value if use_sender_value else contact_dict.get(sender_column, "") - email_client.send_email(sender, recipients, email_subject, email_body_text, attachment_files) + + # 2025-08-27 Andy Holst added cc and bcc fields + email_client.send_email(sender, recipients, cc_recipient, bcc_recipient, email_subject, email_body_text, attachment_files) contact_dict['sendmail_status'] = 'SUCCESS' success += 1 From 8048fd5b64e48ca138eddaec74834cf0ab55af23 Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Wed, 27 Aug 2025 15:33:20 -0600 Subject: [PATCH 06/27] Update dku_email_client.py - Add cc and bcc fields --- python-lib/dku_email_client.py | 49 ++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/python-lib/dku_email_client.py b/python-lib/dku_email_client.py index a1aa136..e6f7c09 100644 --- a/python-lib/dku_email_client.py +++ b/python-lib/dku_email_client.py @@ -43,12 +43,15 @@ class AbstractMessageClient(ABC): def __init__(self, plain_text): self.plain_text = plain_text + # 2025-08-27 Andy Holst added cc and bcc fields @abstractmethod - def send_email(self, sender, recipients, email_body, email_subject, attachment_files): + def send_email(self, sender, recipients, cc_recipient, bcc_recipient, email_body, email_subject, attachment_files): """ Sends a separate email to each recipient :param sender: sender email, str - is ignored if a sender configured for the channel :param recipients: recipients email addresses, list + :param cc_recipient: cc recipient email, str + :param bcc_recipient bcc recipient email, str :param email_subject: str :param email_body: body of either plain text or html, str :param attachment_files:attachments as list of AttachmentFile @@ -79,12 +82,29 @@ def __init__(self, plain_text, channel_id): logging.info(f"Configured channel messaging client with channel {channel_id} - type: {self.channel.type}, " f"sender: {self.channel.sender}, plain_text? {self.plain_text}") - - def send_email(self, sender, recipients, email_subject, email_body, attachment_files): + + # 2025-08-27 Andy Holst added cc and bcc fields + def send_email(self, sender, recipients, cc_recipient, bcc_recipient, email_subject, email_body, attachment_files): files = [(a.file_name, a.data, f"{a.mime_type}/{a.mime_subtype}") for a in attachment_files] sender_to_use = None if self.channel.sender else sender + + # 2025-08-27 Andy Holst added cc and bcc fields + cc_list = cc_recipient.split(",") if cc_recipient != None else [] + bcc_list = bcc_recipient.split(",") if bcc_recipient != None else [] + + # 2025-08-27 Andy Holst added cc and bcc fields for recipient in recipients: - self.channel.send(self.project_id, [recipient], email_subject, email_body, attachments=files, plain_text=self.plain_text, sender=sender_to_use) + all_recipient = [recipient] + cc_list + bcc_list + self.channel.send(self.project_id, + all_recipient, + #[recipient], + email_subject, + email_body, + attachments=files, + plain_text=self.plain_text, + sender=sender_to_use, + cc=cc_list, + bcc=bcc_list) class SmtpEmailClient(AbstractMessageClient): @@ -131,11 +151,14 @@ def attachments_to_mime(self, attachment_files): attachment_mimes.append(mime_app) return attachment_mimes - def send_single_email(self, sender, recipients, email_subject, email_body, attachment_mimes): + # 2025-08-27 Andy Holst added cc and bcc fields + def send_single_email(self, sender, recipients, cc_recipient, bcc_recipient, email_subject, email_body, attachment_mimes): """ Sends a separate email to each recipient :param sender: sender email, str - is ignored if a sender configured for the channel :param recipients: recipients email addresses, list + :param cc_recipient: cc recipient email, str + :param bcc_recipient bcc recipient email, str :param email_subject: str :param email_body: body of either plain text or html, str :param attachment_mimes, list of MIMEBase @@ -143,18 +166,28 @@ def send_single_email(self, sender, recipients, email_subject, email_body, attac msg = MIMEMultipart() msg["From"] = sender msg["To"] = ",".join(recipients) + + # 2025-08-27 Andy Holst added cc and bcc fields + msg["Cc"] = cc_recipient + cc_list = cc_recipient.split(",") if cc_recipient != None else [] + bcc_list = bcc_recipient.split(",") if bcc_recipient != None else [] + all_recipient = [recipients] + cc_list + bcc_list + msg["Subject"] = email_subject body_encoding = "utf-8" text_type = 'plain' if self.plain_text else 'html' msg.attach(MIMEText(email_body, text_type, body_encoding)) for mime_app in attachment_mimes: msg.attach(mime_app) - self.smtp.sendmail(sender, recipients, msg.as_string()) - def send_email(self, sender, recipients, email_subject, email_body, attachment_files): + # 2025-08-27 Andy Holst added cc and bcc fields + self.smtp.sendmail(sender, all_recipient, msg.as_string()) + + # 2025-08-27 Andy Holst added cc and bcc fields + def send_email(self, sender, recipients, cc_recipient, bcc_recipient, email_subject, email_body, attachment_files): attachment_mimes = self.attachments_to_mime(attachment_files) for recipient in recipients: - self.send_single_email(sender, [recipient], email_subject, email_body, attachment_mimes) + self.send_single_email(sender, [recipient], cc_recipient, bcc_recipient, email_subject, email_body, attachment_mimes) def quit(self): """ Do any disconnection needed""" From 40ca76a72944242a6f7dea23e9c72cb1a305d5c5 Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Wed, 27 Aug 2025 15:56:19 -0600 Subject: [PATCH 07/27] Update dku_attachment_handling.py - Add comma-delimited CSV option --- python-lib/dku_attachment_handling.py | 29 ++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/python-lib/dku_attachment_handling.py b/python-lib/dku_attachment_handling.py index 434ee75..5268554 100644 --- a/python-lib/dku_attachment_handling.py +++ b/python-lib/dku_attachment_handling.py @@ -53,22 +53,37 @@ def build_attachment_files(attachment_datasets, attachment_type, apply_coloring_ # Still, if the config has "excel_can_ac" and is run from the flow, we want to treat as excel (it means the user saved in v1.0.0 and did not reopen it) is_excel = attachment_type == "excel" or attachment_type == "excel_can_ac" + # 2025-08-27 Andy Holst added handling of comma-delimited CSV export + is_tab_csv = attachment_type == "csv" + format_params = None if is_excel: request_fmt = "excel" if apply_coloring_excel and supports_messaging_channels_and_conditional_formatting(dataiku.api_client()): format_params = {"applyColoring": True} - else: + # 2025-08-27 Andy Holst added handling of comma-delimited CSV export + #else: + # request_fmt = "tsv-excel-header" + elif is_tab_csv: request_fmt = "tsv-excel-header" # Prepare attachments attachment_files = [] for attachment_ds in attachment_datasets: - with attachment_ds.raw_formatted_data(format=request_fmt, format_params=format_params) as stream: - file_bytes = stream.read() - if is_excel: - attachment_files.append(AttachmentFile(attachment_ds.full_name + ".xlsx", "application", - "vnd.openxmlformats-officedocument.spreadsheetml.sheet", file_bytes)) + # 2025-08-27 Andy Holst added handling of comma-delimited CSV export + if is_excel | is_tab_csv: + with attachment_ds.raw_formatted_data(format=request_fmt, format_params=format_params) as stream: + file_bytes = stream.read() + if is_excel: + attachment_files.append(AttachmentFile(attachment_ds.full_name + ".xlsx", "application", + "vnd.openxmlformats-officedocument.spreadsheetml.sheet", file_bytes)) + else: + attachment_files.append(AttachmentFile(attachment_ds.full_name + ".csv", "text", "csv", file_bytes)) + + # 2025-08-27 Andy Holst added handling of comma-delimited CSV export else: - attachment_files.append(AttachmentFile(attachment_ds.full_name + ".csv", "text", "csv", file_bytes)) + attachment_df = attachment_ds.get_dataframe() + csv_data = attachment_df.to_csv(index = False) + attachment_files.append(AttachmentFile(attachment_ds.full_name + ".csv", "text", "csv", csv_data)) + return attachment_files From deb960edd873bc3894a7db374019605550eb3798 Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Mon, 17 Nov 2025 07:08:01 -0700 Subject: [PATCH 08/27] Update CHANGELOG.md Address comments --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f269bd..b2cfd3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Version 1.0.3A]() - Custom feature release 2025-08 +## [Version 1.0.4]() - Custom feature release 2025-11 - Add comma-delimited CSV option - Add cc and bcc fields From f101ccb1defa18f5b6bc5f5276dae556d862c326 Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Mon, 17 Nov 2025 07:09:44 -0700 Subject: [PATCH 09/27] Update recipe.json Address comment --- custom-recipes/send-mails-from-contacts-dataset/recipe.json | 1 - 1 file changed, 1 deletion(-) diff --git a/custom-recipes/send-mails-from-contacts-dataset/recipe.json b/custom-recipes/send-mails-from-contacts-dataset/recipe.json index 0a562d8..085dfcd 100644 --- a/custom-recipes/send-mails-from-contacts-dataset/recipe.json +++ b/custom-recipes/send-mails-from-contacts-dataset/recipe.json @@ -84,7 +84,6 @@ "type": "PASSWORD", "visibilityCondition" : "(model.mail_channel == null || model.mail_channel == '__DKU__DIRECT_SMTP__') && model.smtp_use_auth" }, - // 2025-08-27 Andy Holst added cc and bcc fields, and adjusted display order { "name": "sender_column", "label" : "Sender (column)", From 314c65c19e19ed372fd15c11d7623d93da42bd01 Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Mon, 17 Nov 2025 07:11:20 -0700 Subject: [PATCH 10/27] Update recipe.json Address comment --- custom-recipes/send-mails-from-contacts-dataset/recipe.json | 1 - 1 file changed, 1 deletion(-) diff --git a/custom-recipes/send-mails-from-contacts-dataset/recipe.json b/custom-recipes/send-mails-from-contacts-dataset/recipe.json index 085dfcd..9657715 100644 --- a/custom-recipes/send-mails-from-contacts-dataset/recipe.json +++ b/custom-recipes/send-mails-from-contacts-dataset/recipe.json @@ -207,7 +207,6 @@ "label": "Attachments", "type": "SEPARATOR" }, - // 2025-08-27 Andy Holst added CSV comma-delimited { "name": "attachment_type", "label" : "Attachments format", From 5143cb02f23db750200bb6461998f4de4dfce9c7 Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Mon, 17 Nov 2025 10:06:15 -0700 Subject: [PATCH 11/27] Update recipe.py Address comments --- .../recipe.py | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/custom-recipes/send-mails-from-contacts-dataset/recipe.py b/custom-recipes/send-mails-from-contacts-dataset/recipe.py index 43707b5..a532d3d 100644 --- a/custom-recipes/send-mails-from-contacts-dataset/recipe.py +++ b/custom-recipes/send-mails-from-contacts-dataset/recipe.py @@ -59,7 +59,6 @@ def parse_recipients(recipients): recipient_column = config.get('recipient_column', None) -# 2025-08-27 Andy Holst added cc and bcc fields cc_column = config.get('cc_column', None) bcc_column = config.get('bcc_column', None) @@ -124,9 +123,21 @@ def parse_recipients(recipients): if not channel_has_sender and not use_sender_value and sender_column not in people_columns: raise AttributeError("The column you specified for sender (%s) was not found." % sender_column) -if recipient_column not in people_columns: - raise AttributeError("The column you specified for recipient (%s) was not found." % recipient_column) - +for arg in [recipient_column, cc_column, bcc_column]: + if not arg: + # recipient_column would have previously failed in Validation Part 1 + pass + elif arg not in people_columns: + raise AttributeError("The column you specified (%s) was not found." % arg) + +# Validation part 3 - validate that recipient, cc, and bcc are string datatypes +people_schema = people.read_schema() + +for check in [recipient_column, cc_column, bcc_column]: + for item in people_schema: + if item['name'] == check: + if item['type'] != 'string': + raise AttributeError("The column you specified for (%s) was not datatype string." % item['name']) # Create Jinja templates if needed @@ -163,34 +174,27 @@ def parse_recipients(recipients): fail = 0 try: for contact in people.iter_rows(): - recipients_string = contact[recipient_column] + recipients_string = contact[recipient_column] + cc_string = contact[cc_column] if cc_column else None + bcc_string = contact[bcc_column] if bcc_column else None if recipients_string: logging.info("Sending to %s" % recipients_string) else: logging.info("No recipient for row - emailing will fail - row data: %s" % contact) contact_dict = dict(contact) - - # 2025-08-27 Andy Holst added cc and bcc fields - if cc_column: - cc_recipient = contact[cc_column] if contact[cc_column] != "" else None - else: - cc_recipient = None - - if bcc_column: - bcc_recipient = contact[bcc_column] if contact[bcc_column] != "" else None - else: - bcc_recipient = None try: email_subject = build_email_subject(use_subject_value, subject_template, subject_column, contact_dict) email_body_text = build_email_message_text(use_body_value, body_template, attachments_templating_dict, contact_dict, body_column, use_html_body_value) recipients = parse_recipients(recipients_string) + cc_recipients = parse_recipients(cc_string) if cc_string else None + bcc_recipients = parse_recipients(bcc_string) if bcc_string else None + # Note - if the channel has a sender configured, the sender value will be ignored by the email client here sender = sender_value if use_sender_value else contact_dict.get(sender_column, "") - # 2025-08-27 Andy Holst added cc and bcc fields - email_client.send_email(sender, recipients, cc_recipient, bcc_recipient, email_subject, email_body_text, attachment_files) + email_client.send_email(sender, recipients, cc_recipients, bcc_recipients, email_subject, email_body_text, attachment_files) contact_dict['sendmail_status'] = 'SUCCESS' success += 1 From fc3de7f86435b3cb704004107a33b0eed4903422 Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Mon, 17 Nov 2025 10:59:57 -0700 Subject: [PATCH 12/27] Update dku_email_client.py Address Liam's comments --- python-lib/dku_email_client.py | 47 ++++++++++++---------------------- 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/python-lib/dku_email_client.py b/python-lib/dku_email_client.py index e6f7c09..afd261b 100644 --- a/python-lib/dku_email_client.py +++ b/python-lib/dku_email_client.py @@ -43,15 +43,14 @@ class AbstractMessageClient(ABC): def __init__(self, plain_text): self.plain_text = plain_text - # 2025-08-27 Andy Holst added cc and bcc fields @abstractmethod - def send_email(self, sender, recipients, cc_recipient, bcc_recipient, email_body, email_subject, attachment_files): + def send_email(self, sender, recipients, cc_recipients, bcc_recipients, email_body, email_subject, attachment_files): """ Sends a separate email to each recipient :param sender: sender email, str - is ignored if a sender configured for the channel :param recipients: recipients email addresses, list - :param cc_recipient: cc recipient email, str - :param bcc_recipient bcc recipient email, str + :param cc_recipients: cc recipient emails, list + :param bcc_recipients bcc recipient emails, list :param email_subject: str :param email_body: body of either plain text or html, str :param attachment_files:attachments as list of AttachmentFile @@ -83,28 +82,20 @@ def __init__(self, plain_text, channel_id): logging.info(f"Configured channel messaging client with channel {channel_id} - type: {self.channel.type}, " f"sender: {self.channel.sender}, plain_text? {self.plain_text}") - # 2025-08-27 Andy Holst added cc and bcc fields - def send_email(self, sender, recipients, cc_recipient, bcc_recipient, email_subject, email_body, attachment_files): + def send_email(self, sender, recipients, cc_recipients, bcc_recipients, email_subject, email_body, attachment_files): files = [(a.file_name, a.data, f"{a.mime_type}/{a.mime_subtype}") for a in attachment_files] sender_to_use = None if self.channel.sender else sender - # 2025-08-27 Andy Holst added cc and bcc fields - cc_list = cc_recipient.split(",") if cc_recipient != None else [] - bcc_list = bcc_recipient.split(",") if bcc_recipient != None else [] - - # 2025-08-27 Andy Holst added cc and bcc fields for recipient in recipients: - all_recipient = [recipient] + cc_list + bcc_list - self.channel.send(self.project_id, - all_recipient, - #[recipient], + self.channel.send(self.project_id, + [recipient], email_subject, email_body, attachments=files, plain_text=self.plain_text, sender=sender_to_use, - cc=cc_list, - bcc=bcc_list) + cc=cc_recipients, + bcc=bcc_recipients) class SmtpEmailClient(AbstractMessageClient): @@ -151,14 +142,13 @@ def attachments_to_mime(self, attachment_files): attachment_mimes.append(mime_app) return attachment_mimes - # 2025-08-27 Andy Holst added cc and bcc fields - def send_single_email(self, sender, recipients, cc_recipient, bcc_recipient, email_subject, email_body, attachment_mimes): + def send_single_email(self, sender, recipients, cc_recipients, bcc_recipients, email_subject, email_body, attachment_mimes): """ Sends a separate email to each recipient :param sender: sender email, str - is ignored if a sender configured for the channel :param recipients: recipients email addresses, list - :param cc_recipient: cc recipient email, str - :param bcc_recipient bcc recipient email, str + :param cc_recipients: cc recipient emails, list + :param bcc_recipients: bcc recipient emails, list :param email_subject: str :param email_body: body of either plain text or html, str :param attachment_mimes, list of MIMEBase @@ -167,11 +157,8 @@ def send_single_email(self, sender, recipients, cc_recipient, bcc_recipient, ema msg["From"] = sender msg["To"] = ",".join(recipients) - # 2025-08-27 Andy Holst added cc and bcc fields - msg["Cc"] = cc_recipient - cc_list = cc_recipient.split(",") if cc_recipient != None else [] - bcc_list = bcc_recipient.split(",") if bcc_recipient != None else [] - all_recipient = [recipients] + cc_list + bcc_list + msg["Cc"] = ",".join(cc_recipients) + all_recipients = [recipients] + cc_recipients + bcc_recipients msg["Subject"] = email_subject body_encoding = "utf-8" @@ -180,14 +167,12 @@ def send_single_email(self, sender, recipients, cc_recipient, bcc_recipient, ema for mime_app in attachment_mimes: msg.attach(mime_app) - # 2025-08-27 Andy Holst added cc and bcc fields - self.smtp.sendmail(sender, all_recipient, msg.as_string()) + self.smtp.sendmail(sender, all_recipients, msg.as_string()) - # 2025-08-27 Andy Holst added cc and bcc fields - def send_email(self, sender, recipients, cc_recipient, bcc_recipient, email_subject, email_body, attachment_files): + def send_email(self, sender, recipients, cc_recipients, bcc_recipients, email_subject, email_body, attachment_files): attachment_mimes = self.attachments_to_mime(attachment_files) for recipient in recipients: - self.send_single_email(sender, [recipient], cc_recipient, bcc_recipient, email_subject, email_body, attachment_mimes) + self.send_single_email(sender, [recipient], cc_recipients, bcc_recipients, email_subject, email_body, attachment_mimes) def quit(self): """ Do any disconnection needed""" From 13e15cacab0010345424e66c4361041d71da4ef4 Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Mon, 17 Nov 2025 11:01:17 -0700 Subject: [PATCH 13/27] Update plugin.json Updated to V1.0.4 --- plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin.json b/plugin.json index 1b4d176..c4ede48 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "id": "sendmail", - "version": "1.0.3A", + "version": "1.0.4", "meta": { "label": "Send emails", "description": "Send emails based on a dataset containing a list of contacts, with optional attachments (other datasets)", From 4fc11c5951a13adc1509e91b9226f9beff0286f1 Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Mon, 17 Nov 2025 11:21:53 -0700 Subject: [PATCH 14/27] Update dku_attachment_handling.py Updated method per coordination with Liam --- python-lib/dku_attachment_handling.py | 34 ++++++++++----------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/python-lib/dku_attachment_handling.py b/python-lib/dku_attachment_handling.py index 5268554..dfd31da 100644 --- a/python-lib/dku_attachment_handling.py +++ b/python-lib/dku_attachment_handling.py @@ -53,37 +53,27 @@ def build_attachment_files(attachment_datasets, attachment_type, apply_coloring_ # Still, if the config has "excel_can_ac" and is run from the flow, we want to treat as excel (it means the user saved in v1.0.0 and did not reopen it) is_excel = attachment_type == "excel" or attachment_type == "excel_can_ac" - # 2025-08-27 Andy Holst added handling of comma-delimited CSV export - is_tab_csv = attachment_type == "csv" - format_params = None if is_excel: request_fmt = "excel" if apply_coloring_excel and supports_messaging_channels_and_conditional_formatting(dataiku.api_client()): format_params = {"applyColoring": True} - # 2025-08-27 Andy Holst added handling of comma-delimited CSV export - #else: - # request_fmt = "tsv-excel-header" - elif is_tab_csv: + elif attachment_type == "csv_comma": + request_fmt = "csv" + format_params={"style": "excel", "separator": ",", "quoteChar" : "\"", "parseHeaderRow": True} + else: request_fmt = "tsv-excel-header" # Prepare attachments attachment_files = [] for attachment_ds in attachment_datasets: - # 2025-08-27 Andy Holst added handling of comma-delimited CSV export - if is_excel | is_tab_csv: - with attachment_ds.raw_formatted_data(format=request_fmt, format_params=format_params) as stream: - file_bytes = stream.read() - if is_excel: - attachment_files.append(AttachmentFile(attachment_ds.full_name + ".xlsx", "application", - "vnd.openxmlformats-officedocument.spreadsheetml.sheet", file_bytes)) - else: - attachment_files.append(AttachmentFile(attachment_ds.full_name + ".csv", "text", "csv", file_bytes)) - - # 2025-08-27 Andy Holst added handling of comma-delimited CSV export + for attachment_ds in attachment_datasets: + with attachment_ds.raw_formatted_data(format=request_fmt, format_params=format_params) as stream: + file_bytes = stream.read() + if is_excel: + attachment_files.append(AttachmentFile(attachment_ds.full_name + ".xlsx", "application", + "vnd.openxmlformats-officedocument.spreadsheetml.sheet", file_bytes)) + else: - attachment_df = attachment_ds.get_dataframe() - csv_data = attachment_df.to_csv(index = False) - attachment_files.append(AttachmentFile(attachment_ds.full_name + ".csv", "text", "csv", csv_data)) - + attachment_files.append(AttachmentFile(attachment_ds.full_name + ".csv", "text", "csv", file_bytes)) return attachment_files From 490ccef270fee3f11339bc4444ee3892142128f7 Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Mon, 17 Nov 2025 12:03:55 -0700 Subject: [PATCH 15/27] Update recipe.py Fixed bug in handling empty cc and bcc --- custom-recipes/send-mails-from-contacts-dataset/recipe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom-recipes/send-mails-from-contacts-dataset/recipe.py b/custom-recipes/send-mails-from-contacts-dataset/recipe.py index a532d3d..1e71595 100644 --- a/custom-recipes/send-mails-from-contacts-dataset/recipe.py +++ b/custom-recipes/send-mails-from-contacts-dataset/recipe.py @@ -188,8 +188,8 @@ def parse_recipients(recipients): email_body_text = build_email_message_text(use_body_value, body_template, attachments_templating_dict, contact_dict, body_column, use_html_body_value) recipients = parse_recipients(recipients_string) - cc_recipients = parse_recipients(cc_string) if cc_string else None - bcc_recipients = parse_recipients(bcc_string) if bcc_string else None + cc_recipients = parse_recipients(cc_string) if cc_string else [""] + bcc_recipients = parse_recipients(bcc_string) if bcc_string else [""] # Note - if the channel has a sender configured, the sender value will be ignored by the email client here sender = sender_value if use_sender_value else contact_dict.get(sender_column, "") From f2c66c24949a16f5173def5e15455c98ed630674 Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Mon, 17 Nov 2025 12:23:37 -0700 Subject: [PATCH 16/27] Update dku_email_client.py Fix list within list --- python-lib/dku_email_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python-lib/dku_email_client.py b/python-lib/dku_email_client.py index afd261b..21d514b 100644 --- a/python-lib/dku_email_client.py +++ b/python-lib/dku_email_client.py @@ -158,7 +158,7 @@ def send_single_email(self, sender, recipients, cc_recipients, bcc_recipients, e msg["To"] = ",".join(recipients) msg["Cc"] = ",".join(cc_recipients) - all_recipients = [recipients] + cc_recipients + bcc_recipients + all_recipients = recipients + cc_recipients + bcc_recipients msg["Subject"] = email_subject body_encoding = "utf-8" From 36e0e510c0cf3f84f561acec85f7e800c5d2333c Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Wed, 19 Nov 2025 14:21:05 -0700 Subject: [PATCH 17/27] Update recipe.py Address comment --- .../recipe.py | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/custom-recipes/send-mails-from-contacts-dataset/recipe.py b/custom-recipes/send-mails-from-contacts-dataset/recipe.py index 1e71595..5ad9ffa 100644 --- a/custom-recipes/send-mails-from-contacts-dataset/recipe.py +++ b/custom-recipes/send-mails-from-contacts-dataset/recipe.py @@ -46,6 +46,14 @@ def parse_recipients(recipients): # Other cases - either a single value or comma separated string `name@place.com, name2@place.com` return recipients.split(",") +# Validates that columns (eg recipient, cc, bcc) are string datatype +def confirm_string_column(column_name, schema): + column = next(filter(lambda col: col['name'] == column_name, schema), None) + if not column: + raise AttributeError("The column you specified (%s) was not found." % column_name) + if column['type'] != 'string': + raise AttributeError("The column you specified (%s) was not datatype string." % column_name) + # Get handles on datasets output_A_names = get_output_names_for_role('output') output = dataiku.Dataset(output_A_names[0]) if len(output_A_names) > 0 else None @@ -114,30 +122,21 @@ def parse_recipients(recipients): raise AttributeError("No value provided for the recipient") -# Validation part 2 - when necessary, check the column values provided are in the contacts (people) dataset -people_columns = [p['name'] for p in people.read_schema()] +# Validation part 2 - when necessary, check the columns given are present as string columns in the contacts (people) dataset +people_schema = people.read_schema() for arg in ['subject', 'body']: - if not globals()["use_" + arg + "_value"] and globals()[arg + "_column"] not in people_columns: - raise AttributeError("The column you specified for %s (%s) was not found." % (arg, globals()[arg + "_column"])) + if not globals()["use_" + arg + "_value"]: + confirm_string_column(globals()[arg + "_column"], people_schema) -if not channel_has_sender and not use_sender_value and sender_column not in people_columns: - raise AttributeError("The column you specified for sender (%s) was not found." % sender_column) +if not channel_has_sender and not use_sender_value: + confirm_string_column(sender_column, people_schema) for arg in [recipient_column, cc_column, bcc_column]: - if not arg: - # recipient_column would have previously failed in Validation Part 1 - pass - elif arg not in people_columns: - raise AttributeError("The column you specified (%s) was not found." % arg) - -# Validation part 3 - validate that recipient, cc, and bcc are string datatypes -people_schema = people.read_schema() - -for check in [recipient_column, cc_column, bcc_column]: - for item in people_schema: - if item['name'] == check: - if item['type'] != 'string': - raise AttributeError("The column you specified for (%s) was not datatype string." % item['name']) + if not arg: + # recipient_column would have previously failed in Validation Part 1 + pass + else: + confirm_string_column(arg, people_schema) # Create Jinja templates if needed From feb3dd43998027aa326524657091439f04f83f4b Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Wed, 19 Nov 2025 14:23:12 -0700 Subject: [PATCH 18/27] Update dku_attachment_handling.py Address comments --- python-lib/dku_attachment_handling.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python-lib/dku_attachment_handling.py b/python-lib/dku_attachment_handling.py index dfd31da..6dbe53b 100644 --- a/python-lib/dku_attachment_handling.py +++ b/python-lib/dku_attachment_handling.py @@ -61,12 +61,13 @@ def build_attachment_files(attachment_datasets, attachment_type, apply_coloring_ elif attachment_type == "csv_comma": request_fmt = "csv" format_params={"style": "excel", "separator": ",", "quoteChar" : "\"", "parseHeaderRow": True} + + # We expect attachment_type == "csv" here, revisit if future types/options are added in future else: request_fmt = "tsv-excel-header" # Prepare attachments attachment_files = [] - for attachment_ds in attachment_datasets: for attachment_ds in attachment_datasets: with attachment_ds.raw_formatted_data(format=request_fmt, format_params=format_params) as stream: file_bytes = stream.read() From 9ea898bbde87e3f8bb685b4a5609a548a7b5bf96 Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Wed, 19 Nov 2025 14:23:52 -0700 Subject: [PATCH 19/27] Update CHANGELOG.md Address comment --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2cfd3b..732414c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Version 1.0.4]() - Custom feature release 2025-11 +## [Version 1.0.4]() - Feature release - 2025-11 - Add comma-delimited CSV option - Add cc and bcc fields From 570495c7e089ec196b71197035a7b1a0efbd5582 Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Wed, 19 Nov 2025 14:34:02 -0700 Subject: [PATCH 20/27] Update dku_email_client.py Address comments --- python-lib/dku_email_client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python-lib/dku_email_client.py b/python-lib/dku_email_client.py index 21d514b..5ac8f58 100644 --- a/python-lib/dku_email_client.py +++ b/python-lib/dku_email_client.py @@ -157,6 +157,8 @@ def send_single_email(self, sender, recipients, cc_recipients, bcc_recipients, e msg["From"] = sender msg["To"] = ",".join(recipients) + # Note: bcc recipients are implicitly handled, by excluding from msg but including in all_recipients + # See: https://stackoverflow.com/questions/1546367/how-to-send-mail-with-to-cc-and-bcc#:~:text=2%20Comments-,Add%20a%20comment,is%20in%20textfile%20for%20reading. msg["Cc"] = ",".join(cc_recipients) all_recipients = recipients + cc_recipients + bcc_recipients From 248a0e594b41be53fa593b1380155784f5496e57 Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Wed, 17 Dec 2025 13:41:56 -0700 Subject: [PATCH 21/27] Update CHANGELOG.md sendmail_changes.patch --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 732414c..0a1e7a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Version 1.0.4]() - Feature release - 2025-11 +## [Version 1.0.4](https://github.com/dataiku/dss-plugin-sendmail/releases/tag/v1.0.4) - Feature release - 2025-12 - Add comma-delimited CSV option - Add cc and bcc fields From c2ba2021c9560c3dc871d031259e5a732b0d4d0b Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Wed, 17 Dec 2025 13:55:14 -0700 Subject: [PATCH 22/27] Update recipe.py sendmail_changes.patch --- .../recipe.py | 50 +++++++++++++------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/custom-recipes/send-mails-from-contacts-dataset/recipe.py b/custom-recipes/send-mails-from-contacts-dataset/recipe.py index 5ad9ffa..2ea6303 100644 --- a/custom-recipes/send-mails-from-contacts-dataset/recipe.py +++ b/custom-recipes/send-mails-from-contacts-dataset/recipe.py @@ -34,25 +34,33 @@ def to_real_channel_id(channel_id): def does_channel_have_sender(channel_id): return channel_id is not None and channel_id.endswith(SENDER_SUFFIX) -# Takes a string and returns a list of one or more address values +# Takes a string and returns a list of address values, could be empty - any no blank/empty strings are filtered out def parse_recipients(recipients): + if not recipients: + # Handle None and empty strings quickly + return [] + json_array_found = False try: # JSON array case value = json.loads(recipients) if isinstance(value, list): - return value + recipients_list = value + json_array_found = True except json.decoder.JSONDecodeError: pass # Other cases - either a single value or comma separated string `name@place.com, name2@place.com` - return recipients.split(",") + if not json_array_found: + recipients_list = recipients.split(",") -# Validates that columns (eg recipient, cc, bcc) are string datatype -def confirm_string_column(column_name, schema): + filter_out_blanks = lambda item: bool(item) and not (isinstance(item, (str)) and item.isspace()) + filtered_list = list(filter(filter_out_blanks, recipients_list)) + return filtered_list + +# Validates column exists in schema +def confirm_column(column_name, schema): column = next(filter(lambda col: col['name'] == column_name, schema), None) if not column: raise AttributeError("The column you specified (%s) was not found." % column_name) - if column['type'] != 'string': - raise AttributeError("The column you specified (%s) was not datatype string." % column_name) # Get handles on datasets output_A_names = get_output_names_for_role('output') @@ -97,7 +105,7 @@ def confirm_string_column(column_name, schema): attachment_type = config.get('attachment_type', "send_no_attachments") -# Validation part 1 - Check some kind of value/column exists for body, subject, sender and recipient +# Validation part 1 - Check some kind of value/column has been given for body, subject, sender and recipient is_body_present = False if use_body_value: @@ -118,25 +126,37 @@ def confirm_string_column(column_name, schema): if not is_subject_present: raise AttributeError("No value provided for the subject") +is_sender_present = False + +if channel_has_sender: + is_sender_present = True +else: + if use_sender_value: + is_sender_present = bool(sender_value) + else: + is_sender_present = bool(sender_column) +if not is_sender_present: + raise AttributeError("No value provided for the sender") + if not recipient_column: raise AttributeError("No value provided for the recipient") -# Validation part 2 - when necessary, check the columns given are present as string columns in the contacts (people) dataset +# Validation part 2 - when necessary, check the columns given exist in the contacts (people) dataset people_schema = people.read_schema() for arg in ['subject', 'body']: if not globals()["use_" + arg + "_value"]: - confirm_string_column(globals()[arg + "_column"], people_schema) + confirm_column(globals()[arg + "_column"], people_schema) if not channel_has_sender and not use_sender_value: - confirm_string_column(sender_column, people_schema) + confirm_column(sender_column, people_schema) for arg in [recipient_column, cc_column, bcc_column]: if not arg: # recipient_column would have previously failed in Validation Part 1 pass else: - confirm_string_column(arg, people_schema) + confirm_column(arg, people_schema) # Create Jinja templates if needed @@ -187,8 +207,8 @@ def confirm_string_column(column_name, schema): email_body_text = build_email_message_text(use_body_value, body_template, attachments_templating_dict, contact_dict, body_column, use_html_body_value) recipients = parse_recipients(recipients_string) - cc_recipients = parse_recipients(cc_string) if cc_string else [""] - bcc_recipients = parse_recipients(bcc_string) if bcc_string else [""] + cc_recipients = parse_recipients(cc_string) + bcc_recipients = parse_recipients(bcc_string) # Note - if the channel has a sender configured, the sender value will be ignored by the email client here sender = sender_value if use_sender_value else contact_dict.get(sender_column, "") @@ -200,7 +220,7 @@ def confirm_string_column(column_name, schema): if writer: writer.write_row_dict(contact_dict) except Exception as e: - logging.exception("Send failed") + logging.exception("Send failed: %s", str(e)) fail += 1 contact_dict['sendmail_status'] = 'FAILED' contact_dict['sendmail_error'] = str(e) From 1b5bd732a22f1d3b7a518fdf9ba225e7cb4e0d73 Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:00:21 -0700 Subject: [PATCH 23/27] Update dku_email_client.py sendmail_changes.patch --- python-lib/dku_email_client.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/python-lib/dku_email_client.py b/python-lib/dku_email_client.py index 5ac8f58..6b0f6ff 100644 --- a/python-lib/dku_email_client.py +++ b/python-lib/dku_email_client.py @@ -86,6 +86,9 @@ def send_email(self, sender, recipients, cc_recipients, bcc_recipients, email_su files = [(a.file_name, a.data, f"{a.mime_type}/{a.mime_subtype}") for a in attachment_files] sender_to_use = None if self.channel.sender else sender + if not recipients: + raise Exception("No recipients provided to send to") + for recipient in recipients: self.channel.send(self.project_id, [recipient], @@ -169,10 +172,16 @@ def send_single_email(self, sender, recipients, cc_recipients, bcc_recipients, e for mime_app in attachment_mimes: msg.attach(mime_app) - self.smtp.sendmail(sender, all_recipients, msg.as_string()) + try: + self.smtp.sendmail(sender, all_recipients, msg.as_string()) + finally: + # Explicitly reset the session or "Error: nested MAIL command" error is possible + self.smtp.rset() def send_email(self, sender, recipients, cc_recipients, bcc_recipients, email_subject, email_body, attachment_files): attachment_mimes = self.attachments_to_mime(attachment_files) + if not recipients: + raise Exception("No recipients provided to send to") for recipient in recipients: self.send_single_email(sender, [recipient], cc_recipients, bcc_recipients, email_subject, email_body, attachment_mimes) From 16e80db86bd4e08741b398606983289d6947b83e Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Thu, 15 Jan 2026 08:49:58 -0700 Subject: [PATCH 24/27] Update CHANGELOG.md Revise to minor update. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a1e7a0..39fd740 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Version 1.0.4](https://github.com/dataiku/dss-plugin-sendmail/releases/tag/v1.0.4) - Feature release - 2025-12 +## [Version 1.1.0](https://github.com/dataiku/dss-plugin-sendmail/releases/tag/v1.1.0) - Feature release - 2026-01 - Add comma-delimited CSV option - Add cc and bcc fields From 55a8fea5708f87d9b4c7af824489d21b58023191 Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Thu, 15 Jan 2026 08:52:43 -0700 Subject: [PATCH 25/27] Update plugin.json --- plugin.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugin.json b/plugin.json index c4ede48..bfd6ea3 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "id": "sendmail", - "version": "1.0.4", + "version": "1.1.0", "meta": { "label": "Send emails", "description": "Send emails based on a dataset containing a list of contacts, with optional attachments (other datasets)", @@ -8,7 +8,8 @@ "icon": "icon-envelope-alt", "licenseInfo": "Apache Software License", "url": "https://www.dataiku.com/dss/plugins/info/sendmail.html", - "tags": ["Marketing"], - "supportLevel": "NOT_SUPPORTED" + "tags": ["Productivity","Export"], + "supportLevel": "NOT_SUPPORTED", + "category": "Digital Marketing" } } From 51ce6d4236b49e792906185ac8c8d0723275df69 Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Thu, 15 Jan 2026 08:59:54 -0700 Subject: [PATCH 26/27] Update recipe.json Adjusted language for expected data format (recipients, cc, bcc) --- custom-recipes/send-mails-from-contacts-dataset/recipe.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom-recipes/send-mails-from-contacts-dataset/recipe.json b/custom-recipes/send-mails-from-contacts-dataset/recipe.json index 9657715..2776bb7 100644 --- a/custom-recipes/send-mails-from-contacts-dataset/recipe.json +++ b/custom-recipes/send-mails-from-contacts-dataset/recipe.json @@ -111,7 +111,7 @@ "label" : "Recipient (column)", "type": "COLUMN", "columnRole" : "contacts", - "description" : "Recipient of the email (from a column)", + "description" : "Recipient(s) of the email (comma-separated or array, from a column)", "mandatory": true }, { @@ -119,14 +119,14 @@ "label" : "cc (column)", "type": "COLUMN", "columnRole" : "contacts", - "description" : "Courtesy copy (from a column)" + "description" : "Courtesy copy (comma-separated or array, from a column)" }, { "name": "bcc_column", "label" : "bcc (column)", "type": "COLUMN", "columnRole" : "contacts", - "description" : "Blind courtesy copy (from a column)" + "description" : "Blind courtesy copy (comma-separated or array, from a column)" }, { "name": "subject_column", From 0364f2b6fd7ecd3ff4c102bbe91bae3939b3f999 Mon Sep 17 00:00:00 2001 From: Andy Holst <58340227+holstbone@users.noreply.github.com> Date: Wed, 15 Apr 2026 19:23:02 -0600 Subject: [PATCH 27/27] Duplicate note for cc and bcc columns Updated descriptions for cc_column and bcc_column to include a note about duplicate data causing multiple emails. --- custom-recipes/send-mails-from-contacts-dataset/recipe.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom-recipes/send-mails-from-contacts-dataset/recipe.json b/custom-recipes/send-mails-from-contacts-dataset/recipe.json index 2776bb7..2bb81c4 100644 --- a/custom-recipes/send-mails-from-contacts-dataset/recipe.json +++ b/custom-recipes/send-mails-from-contacts-dataset/recipe.json @@ -119,14 +119,14 @@ "label" : "cc (column)", "type": "COLUMN", "columnRole" : "contacts", - "description" : "Courtesy copy (comma-separated or array, from a column)" + "description" : "Courtesy copy (comma-separated or array, from a column). Note: Duplicate data will cause multiple emails." }, { "name": "bcc_column", "label" : "bcc (column)", "type": "COLUMN", "columnRole" : "contacts", - "description" : "Blind courtesy copy (comma-separated or array, from a column)" + "description" : "Blind courtesy copy (comma-separated or array, from a column). Note: Duplicate data will cause multiple emails." }, { "name": "subject_column",