I have GrpahMailer for sending e-mails handled like this:
GraphMailer:
<?php
class graphMailer {
var $tenantID;
var $clientID;
var $clientSecret;
var $Token;
var $baseURL;
function __construct($sTenantID, $sClientID, $sClientSecret) {
$this->tenantID = $sTenantID;
$this->clientID = $sClientID;
$this->clientSecret = $sClientSecret;
$this->baseURL = 'https://graph.microsoft.com/v1.0/';
$this->Token = $this->getToken();
}
function getToken() {
$oauthRequest = 'client_id=' . $this->clientID . '&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default&client_secret=' . $this->clientSecret . '&grant_type=client_credentials';
$reply = $this->sendPostRequest('https://login.microsoftonline.com/' . $this->tenantID . '/oauth2/v2.0/token', $oauthRequest);
$reply = json_decode($reply['data']);
return $reply->access_token;
}
function createMessageJSON($messageArgs, $addMessageEnvelope = False) {
/*
$messageArgs[ subject,
replyTo{'name', 'address'},
toRecipients[]{'name', 'address'},
ccRecipients[]{'name', 'address'},
importance,
conversationId,
body,
images[],
attachments[]
]
*/
$messageArray = array();
if (array_key_exists('toRecipients', $messageArgs)) {
foreach ($messageArgs['toRecipients'] as $recipient) {
if (array_key_exists('name', $recipient)) {
$messageArray['toRecipients'][] = array('emailAddress' => array('name' => $recipient['name'], 'address' => $recipient['address']));
} else {
$messageArray['toRecipients'][] = array('emailAddress' => array('address' => $recipient['address']));
}
}
}
if (array_key_exists('ccRecipients', $messageArgs)) {
foreach ($messageArgs['ccRecipients'] as $recipient) {
if (array_key_exists('name', $recipient)) {
$messageArray['ccRecipients'][] = array('emailAddress' => array('name' => $recipient['name'], 'address' => $recipient['address']));
} else {
$messageArray['ccRecipients'][] = array('emailAddress' => array('address' => $recipient['address']));
}
}
}
if (array_key_exists('bccRecipients', $messageArgs)) {
foreach ($messageArgs['bccRecipients'] as $recipient) {
if (array_key_exists('name', $recipient)) {
$messageArray['bccRecipients'][] = array('emailAddress' => array('name' => $recipient['name'], 'address' => $recipient['address']));
} else {
$messageArray['bccRecipients'][] = array('emailAddress' => array('address' => $recipient['address']));
}
}
}
if (array_key_exists('subject', $messageArgs)) $messageArray['subject'] = $messageArgs['subject'];
if (array_key_exists('importance', $messageArgs)) $messageArray['importance'] = $messageArgs['importance'];
if (isset($messageArgs['replyTo'])) $messageArray['replyTo'] = array(array('emailAddress' => array('name' => $messageArgs['replyTo']['name'], 'address' => $messageArgs['replyTo']['address'])));
if (array_key_exists('body', $messageArgs)) $messageArray['body'] = array('contentType' => 'HTML', 'content' => $messageArgs['body']);
if ($addMessageEnvelope) {
$messageArray = array('message'=>$messageArray);
if (array_key_exists('comment', $messageArgs)) {
$messageArray['comment'] = $messageArgs['comment'];
}
if (count($messageArray['message']) == 0) unset($messageArray['message']);
}
return json_encode($messageArray);
}
function getFolderId($mailbox, $folderName) {
$response = $this->sendGetRequest($this->baseURL .'users/'.$mailbox.'/mailFolders?$select=displayName&$top=100');
$folderList = json_decode($response)->value;
foreach ($folderList as $folder) {
//echo $folder->displayName.PHP_EOL;
if ($folder->displayName == $folderName) {
return $folder->id;
}
}
// Now try subfolders
foreach ($folderList as $folder) {
//echo $folder->displayName.PHP_EOL;
$response = $this->sendGetRequest($this->baseURL .'users/'.$mailbox.'/mailFolders/'.$folder->id.'/childFolders?$select=displayName&$top=100');
$childFolderList = json_decode($response)->value;
foreach ($childFolderList as $childFolder) {
//echo $childFolder->displayName.PHP_EOL;
if ($childFolder->displayName == $folderName) {
return $childFolder->id;
}
}
}
return false;
}
function sendMail($mailbox, $messageArgs, $deleteAfterSend = false) {
if (!$this->Token) {
throw new Exception('No token defined');
}
/*
$messageArgs[ subject,
replyTo{'name', 'address'},
toRecipients[]{'name', 'address'},
ccRecipients[]{'name', 'address'},
importance,
conversationId,
body,
images[],
attachments[]
]
*/
$messageJSON = $this->createMessageJSON($messageArgs);
$response = $this->sendPostRequest($this->baseURL . 'users/' . $mailbox . '/messages', $messageJSON, array('Content-type: application/json'));
//var_dump($response);
$responsedata = json_decode($response['data']);
$messageID = $responsedata->id;
foreach ($messageArgs['images'] as $image) {
$messageJSON = json_encode(array('@odata.type' => '#microsoft.graph.fileAttachment', 'name' => $image['Name'], 'contentBytes' => base64_encode($image['Content']), 'contentType' => $image['ContentType'], 'isInline' => true, 'contentId' => $image['ContentID']));
$response = $this->sendPostRequest($this->baseURL . 'users/' . $mailbox . '/messages/' . $messageID . '/attachments', $messageJSON, array('Content-type: application/json'));
}
foreach ($messageArgs['attachments'] as $attachment) {
$messageJSON = json_encode(array('@odata.type' => '#microsoft.graph.fileAttachment', 'name' => $attachment['Name'], 'contentBytes' => base64_encode($attachment['Content']), 'contentType' => $attachment['ContentType'], 'isInline' => false));
$response = $this->sendPostRequest($this->baseURL . 'users/' . $mailbox . '/messages/' . $messageID . '/attachments', $messageJSON, array('Content-type: application/json'));
}
//Send
$response = $this->sendPostRequest($this->baseURL . 'users/' . $mailbox . '/messages/' . $messageID . '/send', '', array('Content-Length: 0'));
if ($deleteAfterSend) {
//Delete the message if $deleteAfterSend
$this->deleteEmail($mailbox, $messageID, False, "sentitems");
$messageID = true;
}
if ($response['code'] == '202') return $messageID;
return false;
}
To this class I am sending data from my PHP in this format:
<?php
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: POST");
header("Access-Control-Allow-Headers: Content-Type");
error_reporting(E_ALL);
ini_set('display_errors', 1);
require_once '_servername.php';
require_once '_config.php';
require_once 'mailer.php';
require_once 'git.php';
require_once __DIR__ . '/vendor/autoload.php';
try {
dibi::connect($con_arr);
} catch (Exception $ex) {
echo json_encode(['success' => false, 'message' => 'Database connection failed: ' . $ex->getMessage()]);
exit;
}
$github = new Github();
$tenantId = "tenantId";
$clientId = "clientId";
$secret = "secret";
$graphMailer = new graphMailer($tenantId, $clientId, $secret);
try {
$data = $_POST;
$data_db = null;
// Check if an email ID was provided, to retrieve previous data if needed
if (isset($data["email_id"])) {
$data_db = dibi::select('*')->from('sd_email')->where('id = %i', $data['email_id'])->fetch();
$dataFrom = $data_db['fromEmail'];
$dataSubject = $data_db['subject'];
} else {
$dataFrom = null;
$dataSubject = null;
}
// Determine the recipient and subject for the email
$recipient = $data["to"] ?? $dataFrom;
$subject = $data["subject"] ?? $dataSubject;
if (!$recipient) {
echo json_encode(['success' => false, 'message' => 'No recipient specified.']);
exit;
}
// Handle CC recipients
$cc = [];
if (isset($data["cc"])) {
foreach (json_decode($data["cc"]) as $copy) {
$cc[] = ['name' => '', 'address' => $copy];
}
}
// Prepare attachments
$attachments = [];
foreach ($_FILES as $file) {
$attachments[] = [
'Name' => $file['name'],
'ContentType' => $file['type'],
'Content' => file_get_contents($file['tmp_name'])
];
}
// Construct mail arguments
$mailArgs = [
'subject' => $subject,
'replyTo' => ['name' => '', 'address' => $recipient],
'toRecipients' => [['name' => '', 'address' => $recipient]],
'ccRecipients' => $cc,
'importance' => 'normal',
'conversationId' => $data_db['conversationId'] ?? null,
'body' => $data["body"],
'attachments' => $attachments,
];
// Save email data to the database
dibi::insert('sd_email', [
'subject' => $subject,
'conversationId' => $data_db['conversationId'] ?? null,
'body' => $data["body"],
'fromEmail' => 'mail',
'toEmail' => $recipient,
'issue_number' => $data_db['issue_number'] ?? null,
])->execute();
$new_id = dibi::getInsertId();
$messID = $graphMailer->sendMail('mail', $mailArgs);
// Handle success or failure of the email sending
if ($messID && $data_db) {
$github->github_create_comment($data_db['issue_number'], "<html><body><p>ID " . $new_id . ", answer to " . $recipient . "</p></body></html> " . $data["body"]);
echo json_encode(['success' => true, 'message' => 'Email sent successfully.']);
} else {
echo json_encode(['success' => false, 'message' => 'Failed to send email.']);
}
} catch (Exception $ex) {
echo json_encode(['success' => false, 'message' => 'An error occurred: ' . $ex->getMessage()]);
}
From client I use JS to communicate with php like this:
document.addEventListener('DOMContentLoaded', () => {
const editor = new Editor('editor-container', 'body_input');
console.log(editor.quill); // Check if quill is properly initialized
const emailForm = new EmailForm('form_mail', 'body_input', editor);
emailForm.initializeTooltips();
document.getElementById('btn-add-attachment').onclick = () => emailForm.selectAttachment();
});
class Editor {
constructor(editorContainerId, bodyInputId) {
this.bodyInputId = bodyInputId;
this.quill = new Quill(`#${editorContainerId}`, {
theme: 'snow',
modules: {
toolbar: //toolbar
},
placeholder: 'Vytvořte e-mail...',
formats: [//formats]
});
this.initializeAccessibility(editorContainerId);
this.quill.on('text-change', () => this.handleTextChange());
}
initializeAccessibility(editorContainerId) {
const editor = document.querySelector(`#${editorContainerId} .ql-editor`);
if (editor) {
Object.assign(editor, {
role: 'textbox',
'aria-multiline': true,
'aria-label': 'Vytvořte e-mail',
'aria-describedby': 'editor-instructions'
});
} else console.error('Quill editor element not found.');
}
handleTextChange() {
const bodyInput = document.getElementById(this.bodyInputId);
if (bodyInput) {
bodyInput.value = this.quill.root.innerHTML;
} else {
console.error('Body input element not found.');
}
}
clearEditor() {
this.quill.root.innerHTML = '';
}
}
class EmailForm {
constructor(formId, bodyInputId, editor) {
this.form = document.getElementById(formId);
this.bodyInputId = bodyInputId;
this.editor = editor;
this.attachments = []; // Store attachments here
this.inlineImages = []; // Store inline images for processing
this.form.addEventListener('submit', async (event) => {
event.preventDefault();
if (this.validateForm()) await this.handleFormSubmission();
});
}
selectAttachment() {
this.createFileInput((file) => {
if (file) {
this.insertAttachment(file, 'file'); // Specify the type as 'file'
}
});
}
createFileInput(callback) {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.style.display = 'none';
fileInput.addEventListener('change', event => {
const file = event.target.files[0];
callback(file);
});
document.body.appendChild(fileInput);
fileInput.click();
document.body.removeChild(fileInput);
}
insertAttachment(attachment, type) {
const container = document.getElementById("attachments");
const div = document.createElement("div");
div.className = 'attachment-item';
// Handle different types of attachments
if (type === 'file') {
const img = document.createElement("img");
img.src = URL.createObjectURL(attachment);
img.style.maxWidth = "10%";
img.style.height = "auto";
const button = document.createElement("button");
button.type = 'button';
button.className = 'btn btn-outline-danger btn-sm ms-2';
button.innerHTML = '<span>Remove</span>';
button.onclick = () => {
div.remove();
this.attachments = this.attachments.filter(att => att.name !== attachment.name); // Remove from attachments list
};
div.append(img, button);
container.appendChild(div);
this.attachments.push(attachment); // Add file to attachments array
} else if (type === 'base64') {
this.insertAttachmentWithBase64(attachment);
} else if (type === 'cid') {
this.insertAttachmentWithFile(attachment);
}
}
async handleFormSubmission() {
const bodyContent = this.editor.quill.root.innerHTML;
const { updatedBody } = await this.processEmailBody(bodyContent);
const transformedContent = await this.transformQuillContent(updatedBody); // Transform the content if needed
this.setBodyInputValue(transformedContent);
await this.submitForm(await this.prepareFormData());
}
async processEmailBody(body) {
const imgRegex = /<img.*?src="data:image/(jpeg|png|jpg);base64,([^"]*)".*?>/g;
let updatedBody = body;
for (let match; (match = imgRegex.exec(body)); ) {
const base64Image = match[0]; // The entire <img> tag
await this.insertAttachment(base64Image, 'base64'); // Insert as base64
// Generate a CID for the inline image
const cid = await this.generateUniqueCID();
updatedBody = updatedBody.replace(match[0], `<img src="cid:${cid}" alt="Inline Image">`);
}
return { updatedBody };
}
async insertAttachmentWithFile(file) {
const cid = await this.generateUniqueCID();
const formData = new FormData();
formData.append('attachment', file);
formData.append('cid', cid); // Optionally, include CID
const response = await fetch(this.form.action, {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.filePath) {
console.log(`File saved at: ${result.filePath}`);
} else {
console.error('Failed to save image on server:', result.error);
}
}
async insertAttachmentWithBase64(base64Image) {
const cid = await this.generateUniqueCID();
const response = await fetch(this.form.action, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
base64Data: base64Image,
outputFile: `${cid}.jpg` // Save with a CID-based filename
})
});
const result = await response.json();
if (result.filePath) {
console.log(`File saved at: ${result.filePath}`);
} else {
console.error('Failed to save image on server:', result.error);
}
}
async submitForm(formData) {
try {
const response = await fetch(this.form.action, { method: 'POST', body: formData });
const result = await response.json();
alert(result.success ? 'Email sent successfully.' : `Failed to send email: ${result.message}`);
if (result.success) this.editor.clearEditor();
} catch (error) {
console.error('Submission error:', error);
alert('An error occurred while sending the email.');
}
}
setBodyInputValue(bodyContent) {
document.getElementById(this.bodyInputId).value = bodyContent;
}
async transformQuillContent(content) {
try {
const transformedBody = await this.processEmailBody(content);
const container = document.createElement('div');
container.innerHTML = transformedBody;
const transformations = [
//paternsforrelaements
];
for (const transformation of transformations) {
if (transformation.replacement instanceof Function) {
let matches;
while ((matches = transformation.pattern.exec(container.innerHTML)) !== null) {
const replacement = await transformation.replacement(...matches);
container.innerHTML = container.innerHTML.replace(matches[0], replacement);
}
} else {
container.innerHTML = container.innerHTML.replace(transformation.pattern, transformation.replacement);
}
}
return container.innerHTML;
} catch (error) {
this.logError('Error transforming Quill content:', error);
throw error;
}
}
validateForm() {
//bolean e-mail validator handler
}
validateInput(inputElement, errorElement, validationFn) {
//validate
}
validateEditorContent() {
//validate
}
toggleErrorDisplay(element, show) {
//error display
}
isValidEmail(email) {
return /^[^s@]+@[^s@]+.[^s@]+$/.test(email);
}
async generateUniqueCID() {
//generate uniqueCID
}
initializeTooltips() {
//tooltip init
}
async prepareFormData() {
const formData = new FormData(this.form);
for (const attachment of this.attachments) {
formData.append('attachments[]', attachment);
}
return formData;
}
}
And input forms looks like this:
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Emailový Formulář</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="../mailer_portal/css/bootstrap.min.css">
<!-- Custom CSS -->
<link rel="stylesheet" href="../mailer_portal/css/main.css">
<!-- Quill CSS -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/quill.snow.css" rel="stylesheet">
<style>
</style>
</head>
<body>
<div class="container">
<div class="email-container">
<h1 class="text-center mb-4">Emailový Formulář</h1>
<form id="form_mail" enctype="multipart/form-data" action="http://gtfoto.localhost/_proreporty/mailer_portal/sender.php">
<div class="mb-3">
<label for="to" class="form-label">Příjemce:</label>
<input id="to" name="to" type="email" class="form-control" required
aria-describedby="to-error"
aria-label="E-mailová adresa příjemce"
title="Zadejte e-mailovou adresu příjemce"
data-tooltip2="Tip: [email protected]"
placeholder="Zadejte e-mailovou adresu příjemce">
<div id="to-error" class="error-message" style="display:none;color:red;">
<p>Prosím zadejte platnou emailovou adresu ve
<span class="tooltip2" id="to-tooltip">
tvaru:
<span class="tooltip2-text">[email protected]</span>.
</span>
</p>
</div>
</div>
<div class="mb-3">
<label for="cc" class="form-label">Kopie pro příjemce:</label>
<input id="cc" name="cc" type="email" class="form-control"
title="Zadejte e-mailovou adresu příjemců kopií"
placeholder="Zadejte emailové adresy pro kopie oddělené čárkou"
aria-label="E-mailové adresy pro kopie"
multiple>
<div id="cc-error" class="error-message" style="display:none;color:red;">Zadejte emailové adresy pro kopie oddělené čárkou.</div>
</div>
<div class="mb-3">
<label for="subject" class="form-label">Předmět:</label>
<input id="subject" name="subject" type="text" class="form-control" required
aria-label="Předmět e-mailu"
placeholder="Zadejte předmět e-mailu">
<div id="subject-error" class="error-message" style="display:none;color:red;">Please enter a subject for the email.</div>
</div>
<div class="mb-3">
<label for="editor-container" class="form-label">Text:</label>
<div id="editor-container" class="form-control" aria-label="Editor textu e-mailu"></div>
<textarea id="body_input" name="body" style="display:none;" aria-hidden="true"></textarea>
<div id="body-error" class="error-message" style="display:none;color:red;">Please enter at least 5 characters in the email content.</div>
</div>
<!-- Form-wide error message -->
<div id="form-error" class="error-message" style="display:none;color:red;"></div>
<button type="button" id="btn-add-attachment" onclick="selectAttachment(insertAttachment)" class="btn btn-success mb-3">
<span>Přidat přílohu</span>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-paperclip" viewBox="0 0 16 16">
<path d="M6.5 3a2.5 2.5 0 1 1 5 0 2.5 2.5 0 0 1-5 0zM3.273 8.048a4.5 4.5 0 1 1 9 0 1 1 0 0 1-1.537 1.82c-.49.297-.946.459-1.41.569a2 2 0 1 0-3.9 0 1.396 1.396 0 0 0 .002.13 1 1 0 0 1-1.537-1.82zM3 7a3 3 0 0 1 6 0 1 1 0 0 1-2 0 1 1 0 0 0-2 0 3 3 0 0 0 6 0 2 2 0 0 0-4 0 1 1 0 0 1-2 0z"/>
</svg>
</button>
<div id="attachments"></div>
<button type="submit" class="btn btn-primary" aria-label="Odeslat e-mail">Odeslat e-mail</button>
</form>
</div>
</div>
<!-- Quill JS -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/quill.min.js"></script>
<script src="quill-editor.js"></script>
<script src="https://unpkg.com/[email protected]/dist/quill.imageCompressor.min.js"></script>
<script>
Quill.register("modules/imageCompressor", imageCompressor);
</script>
</body>
</html>
The problem which I am facing is that, I am trying to process and send even inlined images and I have already tryed multiple ways, but still wasn’t able to properly handle such solution.
So atm I have decided to save images base64 string which I am recieving from quill into coresponding image files.
To save images into files I am using php:
<?php
// Handle CORS
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Content-Type: application/json');
// Handle OPTIONS request for CORS preflight
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
// Function to handle base64 image data and save it
function saveBase64Image($base64Image, $outputFile, $imageType) {
// Remove data URI scheme
$imageBase64 = preg_replace('/^data:image/w+;base64,/', '', $base64Image);
$imageBase64 = base64_decode($imageBase64);
// Check if decoding was successful
if ($imageBase64 === false) {
throw new Exception('Base64 decode failed');
}
// Use a unique filename if none provided
if (empty($outputFile)) {
$outputFile = uniqid() . '.' . $imageType;
}
// Define the file path
$filePath = __DIR__ . '/' . $outputFile;
// Write the image data to the file
if (file_put_contents($filePath, $imageBase64) === false) {
throw new Exception('Failed to write file');
}
return $filePath;
}
try {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = null;
// Handle application/json input
if (isset($_SERVER['CONTENT_TYPE']) && $_SERVER['CONTENT_TYPE'] === 'application/json') {
$data = json_decode(file_get_contents('php://input'), true);
// Check for JSON parsing errors
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON format']);
exit;
}
} else {
// Handle FormData
$data = [
'base64Data' => $_POST['data'] ?? null,
'outputFile' => $_POST['outputFile'] ?? ''
];
}
// Validate and process base64Data
if (isset($data['base64Data']) && !empty($data['base64Data'])) {
$base64Image = $data['base64Data'];
$outputFile = isset($data['outputFile']) ? basename($data['outputFile']) : '';
// Sanitize output file name
$outputFile = preg_replace('/[/\:*?"<>|]/', '', $outputFile);
// Determine image type from base64 string
if (preg_match('/data:image/(png|jpeg|jpg);base64,/', $base64Image, $matches)) {
$imageType = $matches[1];
} else {
$imageType = 'jpeg'; // Default to 'jpeg' if not determined
}
// Save the image and respond with its file path
$filePath = saveBase64Image($base64Image, $outputFile, $imageType);
echo json_encode(['filePath' => $filePath]);
} else {
// Respond with an error if no base64 image data is provided
http_response_code(400); // Bad Request
echo json_encode(['error' => 'No base64 image data provided.']);
exit;
}
} else {
// Respond with method not allowed if the request method is not POST
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
} catch (Exception $e) {
// Handle any exceptions that occur during processing
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
error_log('Error: ' . $e->getMessage());
}
?>