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 may appear against the Contact but be missing from the Purchase Order. Inline images can also cause the association request to fail.

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_CONNECTION with the client's CRM connection name. It needs access to emails, the relevant modules and file uploads.
  • Change https://www.zohoapis.eu if the client uses another data centre.
  • For the automation, replace YOUR_PO_REFERENCE_FIELD with 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.

Scope: this retains the original function's behaviour: it processes all received emails on that Contact, including older messages. It excludes sent emails.

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;
	v_TargetResolved = false;
	l_Attempts = {1,2};
	v_TargetModule = "";
	v_TargetRecordID = "";
	v_AmbiguousTarget = false;
	v_POReference = "";
	v_JobReference = "";
	v_SuccessCount = 0;
	v_DuplicateCount = 0;
	v_PartialCount = 0;
	v_FailedCount = 0;
	v_SkippedCount = 0;
	v_PageCount = 0;
	v_TotalImagesUploaded = 0;
	v_TotalImagesFailed = 0;
	v_MoreRecords = false;
	v_Subject = ifnull(p_Subject,"").toUpperCase().trim();
	v_CanResolve = false;
	if(p_ContactID != null && v_Subject != "")
	{
		v_CanResolve = true;
	}
	if(v_CanResolve)
	{
		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; retain the supplied client's optional Job fallback.
		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)
				{
					info "PO lookup attempt failed: " + v_LookupError.message;
				}
			}
		}
		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)
				{
					info "Job lookup attempt failed: " + v_LookupError.message;
				}
			}
		}
	}
	if(v_TargetRecordID != "")
	{
		v_TargetResolved = true;
		v_EmailEndpoint = v_BaseURL + "/Contacts/" + v_ContactID + "/Emails";
		v_MoreRecords = true;
		m_EmailListParameters = Map();
		l_ProcessedMessageIDs = List();
		l_PermanentErrors = {"OAUTH_SCOPE_MISMATCH","NO_PERMISSION","AUTHORIZATION_FAILED","INVALID_TOKEN","INVALID_MODULE","INVALID_URL_PATTERN"};
		l_PageNumbers = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100};
		for each v_PageNumber in l_PageNumbers
		{
			if(v_MoreRecords)
			{
				v_ListReady = false;
				for each v_ListAttempt in l_Attempts
				{
					if(v_ListReady == false)
					{
						try
						{
							r_EmailList = invokeurl
							[
								url :v_EmailEndpoint
								type :GET
								parameters:m_EmailListParameters
								connection:"YOUR_CRM_CONNECTION"
							];
							if(r_EmailList.get("Emails") != null)
							{
								v_ListReady = true;
							}
						}
						catch(v_ListError)
						{
							info "Email-list attempt failed: " + v_ListError.message;
						}
					}
				}
				if(v_ListReady)
				{
					v_PageCount = v_PageCount + 1;
					l_EmailSummaries = r_EmailList.get("Emails");
					for each m_EmailSummary in l_EmailSummaries
					{
						try
						{
							v_MessageID = ifnull(m_EmailSummary.get("message_id"),"");
							v_ProcessEmail = false;
							if(m_EmailSummary.get("sent") == false && v_MessageID != "" && l_ProcessedMessageIDs.contains(v_MessageID) == false)
							{
								v_ProcessEmail = true;
							}
							if(v_ProcessEmail)
							{
								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)
										{
											info "Email-content attempt failed: " + v_ViewError.message;
										}
									}
								}
								v_ProcessEmail = false;
								if(l_EmailDetails.isEmpty() == false)
								{
									v_ProcessEmail = true;
								}
								if(v_ProcessEmail)
								{
									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"));
									v_ProcessEmail = false;
									if(ifnull(m_EmailDetails.get("sent"),false) == false && m_EmailSender != null && l_EmailRecipients != null && v_EmailDateTime != null)
									{
										v_ProcessEmail = true;
									}
								}
								if(v_ProcessEmail)
								{
									v_ImagesPartial = false;
									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);
												}
											}
											else
											{
												v_ImagesPartial = true;
											}
										}
										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)
												{
													info "Inline-image attempt failed: " + v_ImageError.message;
												}
											}
										}
										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);
											}
											v_TotalImagesUploaded = v_TotalImagesUploaded + 1;
										}
										else
										{
											v_ImagesPartial = true;
											v_TotalImagesFailed = v_TotalImagesFailed + 1;
										}
									}
									// The working standalone payload is preserved, including its 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);
									m_JsonHeaders = Map();
									m_JsonHeaders.put("Content-Type","application/json");
									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("<","&lt;",true).replaceAll(">","&gt;",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");
														info "Email association: " + v_AssociationCode;
														if(v_AssociationCode == "SUCCESS")
														{
															v_AssociationDone = true;
															v_SuccessCount = v_SuccessCount + 1;
															if(v_ContentMode == "text" || v_ImagesPartial)
															{
																v_PartialCount = v_PartialCount + 1;
															}
														}
														else if(v_AssociationCode == "DUPLICATE_DATA")
														{
															v_AssociationDone = true;
															v_DuplicateCount = v_DuplicateCount + 1;
														}
														else if(l_PermanentErrors.contains(v_AssociationCode))
														{
															v_PermanentFailure = true;
														}
													}
													catch(v_AssociationError)
													{
														info "Association attempt failed: " + v_AssociationError.message;
													}
												}
											}
										}
									}
									if(v_AssociationDone == false)
									{
										v_FailedCount = v_FailedCount + 1;
									}
								}
								else
								{
									v_FailedCount = v_FailedCount + 1;
									info "Email content or mandatory metadata remained unavailable after retry.";
								}
							}
							else
							{
								v_SkippedCount = v_SkippedCount + 1;
							}
						}
						catch(v_EmailError)
						{
							v_FailedCount = v_FailedCount + 1;
							info "Email processing failed; remaining messages will still be attempted: " + v_EmailError.message;
						}
					}
					m_PageInfo = ifnull(r_EmailList.get("info"),Map());
					v_NextIndex = ifnull(m_PageInfo.get("next_index"),"");
					v_MoreRecords = false;
					if(m_PageInfo.get("more_records") == true && v_NextIndex != "")
					{
						v_MoreRecords = true;
					}
					if(v_MoreRecords)
					{
						m_EmailListParameters.put("index",v_NextIndex);
					}
				}
				else
				{
					v_MoreRecords = false;
					v_FailedCount = v_FailedCount + 1;
					info "The email list remained unavailable after retry.";
				}
			}
		}
	}
	else
	{
		info "No association attempted: resolve the missing input or ambiguous/missing target; no record was guessed.";
	}
	m_RunSummary = Map();
	m_RunSummary.put("target_resolved",v_TargetResolved);
	m_RunSummary.put("associated",v_SuccessCount);
	m_RunSummary.put("already_associated",v_DuplicateCount);
	m_RunSummary.put("partial_copies",v_PartialCount);
	m_RunSummary.put("failed",v_FailedCount);
	m_RunSummary.put("skipped",v_SkippedCount);
	m_RunSummary.put("pages_read",v_PageCount);
	m_RunSummary.put("more_pages_remaining",v_MoreRecords);
	m_RunSummary.put("images_uploaded",v_TotalImagesUploaded);
	m_RunSummary.put("image_failures",v_TotalImagesFailed);
	info m_RunSummary;
}

Receive a test reply, check the function's info summary, then open the Purchase Order's Emails related list.

API references: Associate Email, Download Inline Images, Upload Files.