✨ send mails asynchronously (fix #62)
This commit is contained in:
parent
9051f84d32
commit
17ee951df7
|
@ -58,19 +58,28 @@
|
|||
// make sure session isn't locked
|
||||
if(extension_loaded("session")) session_write_close();
|
||||
|
||||
// acquire runlock
|
||||
ignore_user_abort(true);
|
||||
|
||||
// subscribe
|
||||
if(!Newsletter::subscribe(mail_address: $mail_address, verify_key: $verify_key)){
|
||||
if(Newsletter::subscribe(mail_address: $mail_address, verify_key: $verify_key)){
|
||||
http_response_code(200);
|
||||
echo(json_encode([
|
||||
"success" => true
|
||||
]));
|
||||
|
||||
} else {
|
||||
http_response_code(200);
|
||||
echo(json_encode([
|
||||
"success" => false
|
||||
]));
|
||||
die();
|
||||
}
|
||||
|
||||
|
||||
// POSITIVE RESPONSE //
|
||||
http_response_code(200);
|
||||
echo(json_encode([
|
||||
"success" => true
|
||||
]));
|
||||
// EXECUTE WORK //
|
||||
// close connection
|
||||
Newsletter::api_helper_http_close_connection();
|
||||
|
||||
// execute queued work
|
||||
Newsletter::queue_work();
|
||||
?>
|
||||
|
|
|
@ -59,18 +59,24 @@
|
|||
if(extension_loaded("session")) session_write_close();
|
||||
|
||||
// unsubscribe
|
||||
if(!Newsletter::unsubscribe(mail_address: $mail_address, unsubscribe_key: $unsubscribe_key)){
|
||||
if(Newsletter::unsubscribe(mail_address: $mail_address, unsubscribe_key: $unsubscribe_key)){
|
||||
http_response_code(200);
|
||||
echo(json_encode([
|
||||
"success" => true
|
||||
]));
|
||||
|
||||
} else {
|
||||
http_response_code(200);
|
||||
echo(json_encode([
|
||||
"success" => false
|
||||
]));
|
||||
die();
|
||||
}
|
||||
|
||||
|
||||
// POSITIVE RESPONSE //
|
||||
http_response_code(200);
|
||||
echo(json_encode([
|
||||
"success" => true
|
||||
]));
|
||||
// EXECUTE WORK //
|
||||
// close connection
|
||||
Newsletter::api_helper_http_close_connection();
|
||||
|
||||
// execute queued work
|
||||
Newsletter::queue_work();
|
||||
?>
|
||||
|
|
|
@ -57,13 +57,23 @@
|
|||
// make sure session isn't locked
|
||||
if(extension_loaded("session")) session_write_close();
|
||||
|
||||
// verify
|
||||
// acquire runlock
|
||||
ignore_user_abort(true);
|
||||
|
||||
// add verify job to queue
|
||||
Newsletter::verify(mail_address: $mail_address, language: $language);
|
||||
|
||||
|
||||
// POSITIVE RESPONSE //
|
||||
// positive response
|
||||
http_response_code(200);
|
||||
echo(json_encode([
|
||||
"success" => true
|
||||
]));
|
||||
|
||||
|
||||
// EXECUTE WORK //
|
||||
// close connection
|
||||
Newsletter::api_helper_http_close_connection();
|
||||
|
||||
// execute queued work
|
||||
Newsletter::queue_work();
|
||||
?>
|
||||
|
|
|
@ -43,19 +43,20 @@
|
|||
// make sure session isn't locked
|
||||
if(extension_loaded("session")) session_write_close();
|
||||
|
||||
// send
|
||||
if(!Newsletter::send_all(content_name: $content_name)){
|
||||
http_response_code(200);
|
||||
echo(json_encode([
|
||||
"success" => false
|
||||
]));
|
||||
die();
|
||||
}
|
||||
// add jobs to queue
|
||||
Newsletter::send_all(content_name: $content_name);
|
||||
|
||||
|
||||
// POSITIVE RESPONSE //
|
||||
// positive response
|
||||
http_response_code(200);
|
||||
echo(json_encode([
|
||||
"success" => true
|
||||
]));
|
||||
|
||||
|
||||
// EXECUTE WORK //
|
||||
// close connection
|
||||
Newsletter::api_helper_http_close_connection();
|
||||
|
||||
// execute queued work
|
||||
Newsletter::queue_work();
|
||||
?>
|
||||
|
|
|
@ -67,19 +67,20 @@
|
|||
// make sure session isn't locked
|
||||
if(extension_loaded("session")) session_write_close();
|
||||
|
||||
// send
|
||||
if(!Newsletter::send_one(mail_address: $mail_address, content_name: $content_name)){
|
||||
http_response_code(200);
|
||||
echo(json_encode([
|
||||
"success" => false
|
||||
]));
|
||||
die();
|
||||
}
|
||||
// add job to queue
|
||||
Newsletter::send_one(mail_address: $mail_address, content_name: $content_name);
|
||||
|
||||
|
||||
// POSITIVE RESPONSE //
|
||||
// positive response
|
||||
http_response_code(200);
|
||||
echo(json_encode([
|
||||
"success" => true
|
||||
]));
|
||||
|
||||
|
||||
// EXECUTE WORK //
|
||||
// close connection
|
||||
Newsletter::api_helper_http_close_connection();
|
||||
|
||||
// execute queued work
|
||||
Newsletter::queue_work();
|
||||
?>
|
||||
|
|
|
@ -155,15 +155,15 @@
|
|||
<div id="newsletter-send-one-form-feedback" class="form-feedback gone">
|
||||
<div id="newsletter-send-one-form-feedback-wait" class="form-feedback-wait centertext gone">
|
||||
<span class="icon spinning ti ti-loader-2"></span>
|
||||
<span class="text">Sending</span>
|
||||
<span class="text">Adding job to queue</span>
|
||||
</div>
|
||||
<div id="newsletter-send-one-form-feedback-success" class="form-feedback-success centertext gone">
|
||||
<span class="icon ti ti-check"></span>
|
||||
<span class="text">Successfully sent</span>
|
||||
<span class="text">Job successfully queued</span>
|
||||
</div>
|
||||
<div id="newsletter-send-one-form-feedback-failure" class="form-feedback-failure centertext gone">
|
||||
<span class="icon ti ti-x"></span>
|
||||
<span class="text">Sending failed</span>
|
||||
<span class="text">Queueing job failed</span>
|
||||
</div>
|
||||
<div id="newsletter-send-one-form-feedback-failure-no-member" class="form-feedback-failure centertext gone">
|
||||
<span class="icon ti ti-x"></span>
|
||||
|
@ -202,15 +202,15 @@
|
|||
<div id="newsletter-send-all-form-feedback" class="form-feedback gone">
|
||||
<div id="newsletter-send-all-form-feedback-wait" class="form-feedback-wait centertext gone">
|
||||
<span class="icon spinning ti ti-loader-2"></span>
|
||||
<span class="text">Sending</span>
|
||||
<span class="text">Adding jobs to queue</span>
|
||||
</div>
|
||||
<div id="newsletter-send-all-form-feedback-success" class="form-feedback-success centertext gone">
|
||||
<span class="icon ti ti-check"></span>
|
||||
<span class="text">Successfully sent</span>
|
||||
<span class="text">Jobs successfully queued</span>
|
||||
</div>
|
||||
<div id="newsletter-send-all-form-feedback-failure" class="form-feedback-failure centertext gone">
|
||||
<span class="icon ti ti-x"></span>
|
||||
<span class="text">Sending failed</span>
|
||||
<span class="text">Queueing jobs failed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -89,17 +89,13 @@
|
|||
// close database
|
||||
$newsletter_db->write_close();
|
||||
|
||||
// maybe send verify mail
|
||||
if($do_verify){
|
||||
// send verify mail
|
||||
$content = self::content_file_read("0000-00-00-verify");
|
||||
self::send(mail_address: $mail_address, content: $content, dataset: [
|
||||
"language" => $language,
|
||||
"verify_key" => $verify_key
|
||||
]);
|
||||
|
||||
} else {
|
||||
// fake delay
|
||||
usleep(rand(200, 2000) * 1000);
|
||||
}
|
||||
|
||||
// release runlock
|
||||
|
@ -272,14 +268,8 @@
|
|||
// close database
|
||||
$newsletter_db->write_close();
|
||||
|
||||
if($success){
|
||||
// send welcome mail
|
||||
self::send_one(mail_address: $mail_address, content_name: "0000-00-00-welcome");
|
||||
|
||||
} else {
|
||||
// fake delay
|
||||
usleep(rand(200, 2000) * 1000);
|
||||
}
|
||||
// maybe send welcome mail
|
||||
if($success) self::send_one(mail_address: $mail_address, content_name: "0000-00-00-welcome");
|
||||
|
||||
// release runlock
|
||||
ignore_user_abort($old_ignore_user_abort);
|
||||
|
@ -399,10 +389,8 @@
|
|||
*
|
||||
* @param string $mail_address Member mail address.
|
||||
* @param string $content_name Name of content to send.
|
||||
*
|
||||
* @return bool Whether sending was successful.
|
||||
*/
|
||||
public static function send_one(string $mail_address, string $content_name): bool {
|
||||
public static function send_one(string $mail_address, string $content_name): void {
|
||||
// acquire runlock
|
||||
$old_ignore_user_abort = (bool)ignore_user_abort(true);
|
||||
|
||||
|
@ -410,13 +398,10 @@
|
|||
$content = self::content_file_read($content_name);
|
||||
|
||||
// send content
|
||||
$success = self::send(mail_address: $mail_address, content: $content);
|
||||
self::send(mail_address: $mail_address, content: $content);
|
||||
|
||||
// release runlock
|
||||
ignore_user_abort($old_ignore_user_abort);
|
||||
|
||||
// return success state
|
||||
return $success;
|
||||
}
|
||||
|
||||
|
||||
|
@ -425,10 +410,8 @@
|
|||
* Send content to all newsletter members.
|
||||
*
|
||||
* @param string $content_name Name of content to send.
|
||||
*
|
||||
* @return bool Whether sending was successful.
|
||||
*/
|
||||
public static function send_all(string $content_name): bool {
|
||||
public static function send_all(string $content_name): void {
|
||||
// LOAD LIST FROM DATABASE //
|
||||
// open database
|
||||
$old_ignore_user_abort = (bool)ignore_user_abort(true);
|
||||
|
@ -446,19 +429,15 @@
|
|||
$content = self::content_file_read($content_name);
|
||||
|
||||
// iterate over all list members
|
||||
$success = true;
|
||||
foreach($member_list as $one_member_mail_address => $one_member_dataset){
|
||||
// send to this member
|
||||
if(!self::send(mail_address: $one_member_mail_address, content: $content, dataset: $one_member_dataset)) $success = false;
|
||||
self::send(mail_address: $one_member_mail_address, content: $content, dataset: $one_member_dataset);
|
||||
}
|
||||
|
||||
|
||||
// FINALIZE //
|
||||
// release runlock
|
||||
ignore_user_abort($old_ignore_user_abort);
|
||||
|
||||
// return success status
|
||||
return $success;
|
||||
}
|
||||
|
||||
|
||||
|
@ -478,6 +457,11 @@
|
|||
if(!is_array($db->get("list"))){
|
||||
$db->set("list", []);
|
||||
}
|
||||
|
||||
// job queue
|
||||
if(!is_array($db->get("queue"))){
|
||||
$db->set("queue", []);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -723,10 +707,8 @@
|
|||
* @param array $content Mail content.
|
||||
* @param ?array $dataset Previously read dataset for this member
|
||||
* (this makes it possible to use one database read for multiple sends).
|
||||
*
|
||||
* @return bool Whether process was successful.
|
||||
*/
|
||||
private static function send(string $mail_address, array $content, ?array $dataset = null): bool {
|
||||
private static function send(string $mail_address, array $content, ?array $dataset = null): void {
|
||||
// MAYBE OBTAIN MEMBER DATASET //
|
||||
if($dataset === null){
|
||||
// open
|
||||
|
@ -767,12 +749,192 @@
|
|||
$body = self::content_render(content: $content, dataset: $dataset);
|
||||
|
||||
|
||||
// SEND MAIL //
|
||||
// ADD JOB //
|
||||
// get subject
|
||||
$subject = $content["subject"][$dataset["language"]];
|
||||
|
||||
// send
|
||||
return self::mail_send_html(mail_address: $mail_address, subject: $subject, body: $body);
|
||||
// add to queue
|
||||
self::queue_job_add(type: "mail_send_html", data: [
|
||||
"mail_address" => $mail_address,
|
||||
"subject" => $subject,
|
||||
"body" => $body,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* HELPER: Add a job to the queue.
|
||||
*
|
||||
* @param string $type Job type.
|
||||
* @param array $data Job data.
|
||||
*/
|
||||
private static function queue_job_add(string $type, array $data = []): void {
|
||||
// OPEN DATABASE //
|
||||
// acquire runlock
|
||||
$old_ignore_user_abort = (bool)ignore_user_abort(true);
|
||||
|
||||
// obtain handle
|
||||
$newsletter_db = new Dat("./.dat/newsletter", Dat::MODE_READ_WRITE);
|
||||
|
||||
|
||||
// MAYBE INITIALIZE DATABASE //
|
||||
self::db_init(db: $newsletter_db);
|
||||
|
||||
|
||||
// UPDATE DATABASE //
|
||||
// get existing job ids
|
||||
$used_job_id_list = array_keys($newsletter_db->get("queue"));
|
||||
|
||||
// generate new unique job id
|
||||
$job_id = Id64::unique(length: 32, list: $used_job_id_list);
|
||||
|
||||
// add new job
|
||||
$newsletter_db->set(["queue", $job_id], [
|
||||
"type" => $type,
|
||||
"data" => $data,
|
||||
"retry_count" => 0,
|
||||
"lock" => [
|
||||
"held" => false,
|
||||
"time" => 0,
|
||||
],
|
||||
]);
|
||||
|
||||
|
||||
// FINALIZE //
|
||||
// close database
|
||||
$newsletter_db->write_close();
|
||||
|
||||
// release runlock
|
||||
ignore_user_abort($old_ignore_user_abort);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* HELPER: Close the http connection to the client.
|
||||
*/
|
||||
public static function api_helper_http_close_connection(): void {
|
||||
// make sure cgi process is not killed
|
||||
ignore_user_abort(true);
|
||||
|
||||
// make sure client stops listening
|
||||
header("Connection: close");
|
||||
header("Content-Encoding: none");
|
||||
|
||||
// current buffer is full content
|
||||
header("Content-Length: " . ob_get_length());
|
||||
|
||||
// flush ob to cgi buffer
|
||||
ob_end_flush();
|
||||
|
||||
// flush cgi buffer to client
|
||||
flush();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* CALLBACK: Client connection has been closed, execute queued jobs now.
|
||||
*/
|
||||
public static function queue_work(): void {
|
||||
// INITIALIZE //
|
||||
// acquire runlock
|
||||
$old_ignore_user_abort = (bool)ignore_user_abort(true);
|
||||
|
||||
// get list of available job ids
|
||||
$newsletter_db = new Dat("./.dat/newsletter", Dat::MODE_READ);
|
||||
$job_id_list = array_keys($newsletter_db->get("queue"));
|
||||
$newsletter_db->close();
|
||||
|
||||
|
||||
foreach($job_id_list as $one_job_id){
|
||||
// TRY ACQUIRING A JOB LOCK //
|
||||
// reset timeout
|
||||
set_time_limit(150);
|
||||
|
||||
// open database
|
||||
$newsletter_db = new Dat("./.dat/newsletter", Dat::MODE_READ_WRITE);
|
||||
|
||||
// make sure job still exists
|
||||
$one_job_data = $newsletter_db->get(["queue", $one_job_id]);
|
||||
if($one_job_data === null){
|
||||
$newsletter_db->close();
|
||||
continue;
|
||||
}
|
||||
|
||||
// check whether lock is either not held or stale
|
||||
$time = time();
|
||||
if($one_job_data["lock"]["held"] and $one_job_data["lock"]["time"] + 180 >= $time){
|
||||
$newsletter_db->close();
|
||||
continue;
|
||||
}
|
||||
|
||||
// acquire lock
|
||||
$newsletter_db->set(["queue", $one_job_id, "lock"], [
|
||||
"held" => true,
|
||||
"time" => $time,
|
||||
]);
|
||||
|
||||
// close database
|
||||
$newsletter_db->write_close();
|
||||
|
||||
|
||||
// TRY EXECUTING JOB //
|
||||
$success = false;
|
||||
if($one_job_data["type"] === "mail_send_html"){
|
||||
// `mail_send_html`
|
||||
$success = self::mail_send_html(...$one_job_data["data"]);
|
||||
|
||||
} else {
|
||||
// unknown job type
|
||||
Error::warn(message: "Unknown job type", data: ["type" => $one_job_data["type"], "one_job_id" => $one_job_id, "one_job_data" => $one_job_data]);
|
||||
}
|
||||
|
||||
|
||||
// FINISH THIS JOB //
|
||||
// open database
|
||||
$newsletter_db = new Dat("./.dat/newsletter", Dat::MODE_READ_WRITE);
|
||||
|
||||
// make sure job still exists
|
||||
$one_job_data = $newsletter_db->get(["queue", $one_job_id]);
|
||||
if($one_job_data === null){
|
||||
$newsletter_db->close();
|
||||
continue;
|
||||
}
|
||||
|
||||
// maybe remove successful job
|
||||
if($success){
|
||||
$newsletter_db->unset(["queue", $one_job_id]);
|
||||
$newsletter_db->write_close();
|
||||
continue;
|
||||
}
|
||||
|
||||
// maybe remove failed job with high retry count
|
||||
if(($one_job_data["retry_count"] + 1) >= 3){
|
||||
Error::warn(message: "Multiple failed job execute attempts", data: ["one_job_id" => $one_job_id, "one_job_data" => $one_job_data]);
|
||||
$newsletter_db->unset(["queue", $one_job_id]);
|
||||
$newsletter_db->write_close();
|
||||
continue;
|
||||
}
|
||||
|
||||
// increase retry count
|
||||
$newsletter_db->set(["queue", $one_job_id, "retry_count"], $one_job_data["retry_count"] + 1);
|
||||
|
||||
// release job lock
|
||||
$newsletter_db->set(["queue", $one_job_id, "lock"], [
|
||||
"held" => false,
|
||||
"time" => 0,
|
||||
]);
|
||||
|
||||
// close database
|
||||
$newsletter_db->write_close();
|
||||
}
|
||||
|
||||
|
||||
// FINALIZE //
|
||||
// release runlock
|
||||
ignore_user_abort($old_ignore_user_abort);
|
||||
}
|
||||
|
||||
|
||||
|
@ -792,6 +954,9 @@
|
|||
|
||||
|
||||
// GENERAL SETTINGS //
|
||||
// make timeout more aggressive
|
||||
$phpmailer->Timeout = 30;
|
||||
|
||||
// we use smtp to send our mail
|
||||
$phpmailer->IsSMTP();
|
||||
|
||||
|
|
Loading…
Reference in New Issue