What?
Associate an incoming supplier reply with a Purchase Order so it appears in that order's Emails related list.
Why?
The outgoing email is already associated correctly. The supplier's reply appears in the Contact's email history, but is missing from the Purchase Order. The association can also fail when the reply contains inline images.
How?
Read the reply from the Contact, download and re-upload its inline images, replace their references in the HTML, then associate the email with the Purchase Order.
1. Set up the connection
- Replace
YOUR_CRM_CONNECTIONwith the client's CRM connection name. It needs access to emails, the relevant modules and file uploads. - Change
https://www.zohoapis.euif the client uses another data centre. - For the automation, replace
YOUR_PO_REFERENCE_FIELDwith the Purchase Order reference field's API name.
2. Test the association
Pass the supplier's Contact ID and the destination Purchase Order ID as String arguments. This test uses the first email returned for the Contact; check that it is the reply you want.
string standalone.fn_PurchaseOrders_AssociateContactEmail(String p_ContactID,String p_PurchaseOrderID)
{
v_ApiBase = "https://www.zohoapis.eu/crm/v8";
v_ContactID = ifnull(p_ContactID,"").trim();
v_PurchaseOrderID = ifnull(p_PurchaseOrderID,"").trim();
if(v_ContactID == "" || v_PurchaseOrderID == "")
{
return "Supply both the Contact ID and target Purchase Order ID.";
}
// Fetch the first email from the Contact.
r_Emails = invokeurl
[
url :v_ApiBase + "/Contacts/" + v_ContactID + "/Emails"
type :GET
connection:"YOUR_CRM_CONNECTION"
];
l_Emails = ifnull(r_Emails.get("Emails"),List());
if(l_Emails.isEmpty())
{
return "No source email returned: " + r_Emails.toString();
}
m_Email = l_Emails.get(0);
v_MessageID = m_Email.get("message_id");
m_EmailOwner = ifnull(m_Email.get("owner"),Map());
v_EmailUserID = m_EmailOwner.get("id");
if(v_MessageID == null || v_EmailUserID == null)
{
return "The source email is missing its message ID or owner ID.";
}
// Fetch the original HTML using the owner of the selected email.
m_EmailViewParameters = Map();
m_EmailViewParameters.put("user_id",v_EmailUserID);
r_Content = invokeurl
[
url :v_ApiBase + "/Contacts/" + v_ContactID + "/Emails/" + v_MessageID
type :GET
parameters:m_EmailViewParameters
connection:"YOUR_CRM_CONNECTION"
];
l_EmailContent = ifnull(r_Content.get("Emails"),List());
if(l_EmailContent.isEmpty())
{
return "No email content returned: " + r_Content.toString();
}
m_EmailDetails = l_EmailContent.get(0);
v_EmailContent = ifnull(m_EmailDetails.get("content"),"");
if(v_EmailContent == "")
{
return "The source email has no content.";
}
// Normalize the source email's backslash image references.
v_EmailContent = v_EmailContent.replaceAll("crm[^A-Za-z0-9]+img_id:","crm/img_id:",false);
// Collect distinct inline image IDs from quoted HTML attributes.
// Handles both single and double quotes, including repeated images.
l_InlineImageIDs = List();
l_ImageParts = v_EmailContent.toList("crm/img_id:");
v_PartIndex = 0;
for each v_ImagePart in l_ImageParts
{
if(v_PartIndex > 0)
{
v_EndIndex = v_ImagePart.indexOf("\"");
v_SingleQuoteIndex = v_ImagePart.indexOf("'");
if(v_SingleQuoteIndex >= 0)
{
if(v_EndIndex < 0 || v_SingleQuoteIndex < v_EndIndex)
{
v_EndIndex = v_SingleQuoteIndex;
}
}
if(v_EndIndex <= 0)
{
return "Could not extract an inline image ID from the source HTML.";
}
v_ImageID = v_ImagePart.subString(0,v_EndIndex);
if(l_InlineImageIDs.contains(v_ImageID) == false)
{
l_InlineImageIDs.add(v_ImageID);
}
}
v_PartIndex = v_PartIndex + 1;
}
info "Distinct inline images found: " + l_InlineImageIDs.size();
for each v_ImageID in l_InlineImageIDs
{
// 1. Download the original inline image.
m_ImageDownloadParameters = Map();
m_ImageDownloadParameters.put("message_id",v_MessageID);
m_ImageDownloadParameters.put("user_id",v_EmailUserID);
m_ImageDownloadParameters.put("id",v_ImageID);
r_InlineImageFile = invokeurl
[
url :v_ApiBase + "/Contacts/" + v_ContactID + "/Emails/actions/download_inline_images"
type :GET
parameters:m_ImageDownloadParameters
connection:"YOUR_CRM_CONNECTION"
];
if(r_InlineImageFile.isFile() == false)
{
return "Inline image download failed. Image ID: " + v_ImageID;
}
// 2. Upload as an inline image to ZFS.
// files sends multipart/form-data; do not add the JSON header here.
r_InlineImageFile.setParamName("file");
r_Upload = invokeurl
[
url :v_ApiBase + "/files?type=inline"
type :POST
files:r_InlineImageFile
connection:"YOUR_CRM_CONNECTION"
];
l_UploadResults = ifnull(r_Upload.get("data"),List());
if(l_UploadResults.isEmpty())
{
return "Inline image upload failed: " + r_Upload.toString();
}
m_UploadResult = l_UploadResults.get(0);
if(m_UploadResult.get("code") != "SUCCESS")
{
return "Inline image upload failed: " + m_UploadResult.toString();
}
m_UploadDetails = ifnull(m_UploadResult.get("details"),Map());
v_ZFSFileID = ifnull(m_UploadDetails.get("id"),"");
if(v_ZFSFileID == "")
{
return "The inline image upload returned no file ID: " + r_Upload.toString();
}
// 3. Replace every occurrence using the new ZFS ID.
// Include the closing quote to avoid matching part of another ID.
v_OldImageReference = "crm/img_id:" + v_ImageID;
v_NewImageReference = "crm/img_id:" + v_ZFSFileID;
v_EmailContent = v_EmailContent.replaceAll(v_OldImageReference + "\"",v_NewImageReference + "\"",true);
v_EmailContent = v_EmailContent.replaceAll(v_OldImageReference + "'",v_NewImageReference + "'",true);
}
// Build the association payload after replacing all inline images.
m_AssociationEmail = Map();
m_AssociationEmail.put("from",m_Email.get("from"));
m_AssociationEmail.put("to",m_Email.get("to"));
m_AssociationEmail.put("subject",m_Email.get("subject"));
m_AssociationEmail.put("content",v_EmailContent);
m_AssociationEmail.put("mail_format","html");
m_AssociationEmail.put("date_time",m_Email.get("time"));
m_AssociationEmail.put("sent",m_Email.get("sent"));
// Zoho documents this field as the original email header's Message-ID.
m_AssociationEmail.put("original_message_id",v_MessageID);
l_AssociationEmails = List();
l_AssociationEmails.add(m_AssociationEmail);
m_AssociationRequest = Map();
m_AssociationRequest.put("Emails",l_AssociationEmails);
m_JsonHeaders = Map();
m_JsonHeaders.put("Content-Type","application/json");
// Associate the email with the Purchase Order.
r_Associate = invokeurl
[
url :v_ApiBase + "/Purchase_Orders/" + v_PurchaseOrderID + "/actions/associate_email"
type :POST
parameters:m_AssociationRequest.toString()
headers:m_JsonHeaders
connection:"YOUR_CRM_CONNECTION"
];
info r_Associate;
return r_Associate.toString();
}
3. Run it when a reply arrives
Create an email workflow for Contacts » Incoming email » Received. Add fn_PurchaseOrders_AssociateSupplierReplies and map:
p_Subject(String): the incoming email's subject.p_ContactID(Integer): the supplier Contact's record ID.
The subject selects the order using the configured PO reference, for example PO - DEMO - JOB-DEMO. Adapt the subject parsing to the client's format.
The function checks the first 10 emails returned for the Contact and processes received messages only. It makes no requests for older pages.
Set v_EnableJobFallback to false for Purchase Orders only. Set v_EnableTextFallback to false to prevent a labelled text copy when HTML association fails.
void automation.fn_PurchaseOrders_AssociateSupplierReplies(String p_Subject,Int p_ContactID)
{
v_BaseURL = "https://www.zohoapis.eu/crm/v8";
v_POReferenceField = "YOUR_PO_REFERENCE_FIELD";
v_EnableJobFallback = true;
v_EnableTextFallback = true;
l_Attempts = {1,2};
m_JsonHeaders = Map();
m_JsonHeaders.put("Content-Type","application/json");
v_TargetModule = "";
v_TargetRecordID = "";
v_AmbiguousTarget = false;
v_POReference = "";
v_JobReference = "";
v_Subject = ifnull(p_Subject,"").toUpperCase().trim();
if(p_ContactID != null && v_Subject != "")
{
v_ContactID = p_ContactID.toString();
v_POPosition = v_Subject.indexOf("PO - ");
if(v_POPosition >= 0)
{
v_Remainder = v_Subject.subString(v_POPosition + 5);
l_ReferenceParts = v_Remainder.toList(" - ");
if(l_ReferenceParts.size() > 0)
{
v_POReference = l_ReferenceParts.get(0).trim();
if(v_POReference.startsWith("JOB-"))
{
v_POReference = "";
}
}
}
v_JobPosition = v_Subject.indexOf("JOB-");
if(v_JobPosition >= 0)
{
l_JobParts = v_Subject.subString(v_JobPosition).toList(" ");
if(l_JobParts.size() > 0)
{
v_JobReference = l_JobParts.get(0).trim();
}
}
// Resolve the PO first, then optionally check the Job reference.
for each v_LookupAttempt in l_Attempts
{
if(v_TargetRecordID == "" && v_AmbiguousTarget == false && v_POReference != "")
{
try
{
l_SearchResults = zoho.crm.searchRecords("Purchase_Orders","(" + v_POReferenceField + ":equals:" + v_POReference + ")",1,100,Map(),"YOUR_CRM_CONNECTION");
l_ExactMatches = List();
for each m_SearchResult in l_SearchResults
{
if(ifnull(m_SearchResult.get(v_POReferenceField),"").toString().toUpperCase() == v_POReference)
{
l_ExactMatches.add(m_SearchResult);
}
}
if(l_ExactMatches.size() == 1 && l_SearchResults.size() < 100)
{
v_TargetModule = "Purchase_Orders";
v_TargetRecordID = l_ExactMatches.get(0).get("id").toString();
}
else if(l_ExactMatches.size() > 1 || l_SearchResults.size() >= 100)
{
v_AmbiguousTarget = true;
}
}
catch(v_LookupError)
{
v_TargetRecordID = "";
}
}
}
for each v_LookupAttempt in l_Attempts
{
if(v_EnableJobFallback && v_TargetRecordID == "" && v_AmbiguousTarget == false && v_JobReference != "")
{
try
{
l_SearchResults = zoho.crm.searchRecords("Sales_Orders","(Subject:equals:" + v_JobReference + ")",1,100,Map(),"YOUR_CRM_CONNECTION");
l_ExactMatches = List();
for each m_SearchResult in l_SearchResults
{
l_SubjectTokens = ifnull(m_SearchResult.get("Subject"),"").toUpperCase().replaceAll("[^A-Z0-9_-]"," ",false).toList(" ");
if(l_SubjectTokens.contains(v_JobReference))
{
l_ExactMatches.add(m_SearchResult);
}
}
if(l_ExactMatches.size() == 1 && l_SearchResults.size() < 100)
{
v_TargetModule = "Sales_Orders";
v_TargetRecordID = l_ExactMatches.get(0).get("id").toString();
}
else if(l_ExactMatches.size() > 1 || l_SearchResults.size() >= 100)
{
v_AmbiguousTarget = true;
}
}
catch(v_LookupError)
{
v_TargetRecordID = "";
}
}
}
}
if(v_TargetRecordID != "")
{
v_EmailEndpoint = v_BaseURL + "/Contacts/" + v_ContactID + "/Emails";
l_ProcessedMessageIDs = List();
l_PermanentErrors = {"OAUTH_SCOPE_MISMATCH","NO_PERMISSION","AUTHORIZATION_FAILED","INVALID_TOKEN","INVALID_MODULE","INVALID_URL_PATTERN"};
v_ListReady = false;
for each v_ListAttempt in l_Attempts
{
if(v_ListReady == false)
{
try
{
r_EmailList = invokeurl
[
url :v_EmailEndpoint
type :GET
connection:"YOUR_CRM_CONNECTION"
];
if(r_EmailList.get("Emails") != null)
{
v_ListReady = true;
}
}
catch(v_ListError)
{
v_ListReady = false;
}
}
}
if(v_ListReady)
{
// The Emails API returns ten emails; do not request another page.
l_EmailSummaries = r_EmailList.get("Emails");
for each m_EmailSummary in l_EmailSummaries
{
try
{
v_MessageID = ifnull(m_EmailSummary.get("message_id"),"");
if(m_EmailSummary.get("sent") == false && v_MessageID != "" && l_ProcessedMessageIDs.contains(v_MessageID) == false)
{
l_ProcessedMessageIDs.add(v_MessageID);
m_EmailOwner = ifnull(m_EmailSummary.get("owner"),Map());
v_EmailOwnerID = ifnull(m_EmailOwner.get("id"),"");
m_ViewParameters = Map();
if(v_EmailOwnerID != "")
{
m_ViewParameters.put("user_id",v_EmailOwnerID);
}
l_EmailDetails = List();
for each v_ViewAttempt in l_Attempts
{
if(l_EmailDetails.isEmpty())
{
try
{
r_EmailDetails = invokeurl
[
url :v_EmailEndpoint + "/" + v_MessageID
type :GET
parameters:m_ViewParameters
connection:"YOUR_CRM_CONNECTION"
];
l_EmailDetails = ifnull(r_EmailDetails.get("Emails"),List());
}
catch(v_ViewError)
{
l_EmailDetails = List();
}
}
}
v_ProcessEmail = false;
if(l_EmailDetails.isEmpty() == false)
{
m_EmailDetails = l_EmailDetails.get(0);
v_EmailContent = ifnull(m_EmailDetails.get("content"),"");
v_EmailSubject = ifnull(m_EmailDetails.get("subject"),m_EmailSummary.get("subject"));
v_EmailDateTime = ifnull(m_EmailDetails.get("sent_time"),m_EmailSummary.get("time"));
m_EmailSender = ifnull(m_EmailDetails.get("from"),m_EmailSummary.get("from"));
l_EmailRecipients = ifnull(m_EmailDetails.get("to"),m_EmailSummary.get("to"));
if(ifnull(m_EmailDetails.get("sent"),false) == false && m_EmailSender != null && l_EmailRecipients != null && v_EmailDateTime != null)
{
v_ProcessEmail = true;
}
}
if(v_ProcessEmail)
{
if(v_EmailContent == "")
{
v_EmailContent = "<p>The source email body was empty.</p>";
}
v_EmailContent = v_EmailContent.replaceAll("crm[^A-Za-z0-9]+img_id:","crm/img_id:",false);
l_ImageIDs = List();
v_PartNumber = 0;
for each v_ImagePart in v_EmailContent.toList("crm/img_id:")
{
if(v_PartNumber > 0)
{
v_QuotePosition = v_ImagePart.indexOf("\"");
v_SingleQuotePosition = v_ImagePart.indexOf("'");
if(v_SingleQuotePosition >= 0 && (v_QuotePosition < 0 || v_SingleQuotePosition < v_QuotePosition))
{
v_QuotePosition = v_SingleQuotePosition;
}
if(v_QuotePosition > 0)
{
v_ImageID = v_ImagePart.subString(0,v_QuotePosition);
if(l_ImageIDs.contains(v_ImageID) == false)
{
l_ImageIDs.add(v_ImageID);
}
}
}
v_PartNumber = v_PartNumber + 1;
}
for each v_ImageID in l_ImageIDs
{
v_NewImageID = "";
for each v_ImageAttempt in l_Attempts
{
if(v_NewImageID == "")
{
try
{
m_ImageParameters = Map();
m_ImageParameters.put("message_id",v_MessageID);
m_ImageParameters.put("user_id",v_EmailOwnerID);
m_ImageParameters.put("id",v_ImageID);
r_InlineImageFile = invokeurl
[
url :v_EmailEndpoint + "/actions/download_inline_images"
type :GET
parameters:m_ImageParameters
connection:"YOUR_CRM_CONNECTION"
];
if(r_InlineImageFile.isFile())
{
r_InlineImageFile.setParamName("file");
r_ImageUpload = invokeurl
[
url :v_BaseURL + "/files?type=inline"
type :POST
files:r_InlineImageFile
connection:"YOUR_CRM_CONNECTION"
];
l_UploadResults = ifnull(r_ImageUpload.get("data"),List());
if(l_UploadResults.isEmpty() == false)
{
m_UploadResult = l_UploadResults.get(0);
if(m_UploadResult.get("code") == "SUCCESS")
{
m_UploadDetails = ifnull(m_UploadResult.get("details"),Map());
v_NewImageID = ifnull(m_UploadDetails.get("id"),"");
}
}
}
}
catch(v_ImageError)
{
v_NewImageID = "";
}
}
}
if(v_NewImageID != "")
{
for each v_Quote in {"\"","'"}
{
v_EmailContent = v_EmailContent.replaceAll("crm/img_id:" + v_ImageID + v_Quote,"crm/img_id:" + v_NewImageID + v_Quote,true);
}
}
}
// Associate using the source message ID.
m_AssociationEmail = Map();
m_AssociationEmail.put("from",m_EmailSender);
m_AssociationEmail.put("to",l_EmailRecipients);
m_AssociationEmail.put("subject",v_EmailSubject);
m_AssociationEmail.put("mail_format","html");
m_AssociationEmail.put("date_time",v_EmailDateTime);
m_AssociationEmail.put("sent",false);
m_AssociationEmail.put("original_message_id",v_MessageID);
v_AssociationDone = false;
v_PermanentFailure = false;
for each v_ContentMode in {"original","text"}
{
if(v_AssociationDone == false && v_PermanentFailure == false && (v_ContentMode == "original" || v_EnableTextFallback))
{
v_AttemptContent = v_EmailContent;
if(v_ContentMode == "text")
{
v_TextContent = v_EmailContent.replaceAll("(?is)<(script|style)[^>]*>.*?</(script|style)>","",false);
v_TextContent = v_TextContent.replaceAll("(?i)<br[^>]*>","\n",false).replaceAll("(?i)</(p|div|li)>","\n",false);
v_TextContent = v_TextContent.replaceAll("<[^>]*>","",false).replaceAll("<","<",true).replaceAll(">",">",true);
v_AttemptContent = "<p><em>Text copy: the original formatting or inline images could not be associated.</em></p><pre>" + v_TextContent + "</pre>";
}
m_AssociationEmail.put("content",v_AttemptContent);
l_AssociationEmails = List();
l_AssociationEmails.add(m_AssociationEmail);
m_AssociationRequest = Map();
m_AssociationRequest.put("Emails",l_AssociationEmails);
for each v_AssociationAttempt in l_Attempts
{
if(v_AssociationDone == false && v_PermanentFailure == false)
{
try
{
r_Association = invokeurl
[
url :v_BaseURL + "/" + v_TargetModule + "/" + v_TargetRecordID + "/actions/associate_email"
type :POST
parameters:m_AssociationRequest.toString()
headers:m_JsonHeaders
connection:"YOUR_CRM_CONNECTION"
];
m_AssociationResult = r_Association;
l_AssociationResults = ifnull(r_Association.get("Emails"),List());
if(l_AssociationResults.isEmpty() == false)
{
m_AssociationResult = l_AssociationResults.get(0);
}
v_AssociationCode = ifnull(m_AssociationResult.get("code"),"UNKNOWN");
if(v_AssociationCode == "SUCCESS" || v_AssociationCode == "DUPLICATE_DATA")
{
v_AssociationDone = true;
}
else if(l_PermanentErrors.contains(v_AssociationCode))
{
v_PermanentFailure = true;
}
}
catch(v_AssociationError)
{
v_AssociationDone = false;
}
}
}
}
}
}
}
}
catch(v_EmailError)
{
v_ProcessEmail = false;
}
}
}
}
}
Receive a test reply, then open the Purchase Order's Emails related list.
API references: Get Emails (10 per call), Associate Email, Download Inline Images, Upload Files.
Discussion
Comments
Questions, corrections and useful additions are reviewed before appearing here.
Discussion Newest first
No comments yet. Start the discussion.