diff --git a/Message.pm b/Message.pm new file mode 100644 index 0000000..feee5e4 --- /dev/null +++ b/Message.pm @@ -0,0 +1,43 @@ +## @file +# This file contains the implementation of the base Message class. +# +# @author Chris Page <chris@starforge.co.uk> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +## @class Message +# This is the 'base' class for the Message modules. It provides any functionality +# that needs to be shared between the Message::* modules. +package Message; +use strict; +use base qw(SystemModule); + +# ============================================================================ +# Constructor + +## @cmethod Message new(%args) +# Create a new Message object. This will create an Message object that may be +# used to store messages to send at a later date, or invoked to send messages +# immediately or from the queue. +# +# @param args A hash of arguments to initialise the Message object with. +# @return A new Message object. +sub new { + my $invocant = shift; + my $class = ref($invocant) || $invocant; + + return $class -> SUPER::new(@_); +} + +1; diff --git a/Message/Queue.pm b/Message/Queue.pm new file mode 100644 index 0000000..36a25cc --- /dev/null +++ b/Message/Queue.pm @@ -0,0 +1,244 @@ +## @file +# This file contains the implementation of the Message queue class. +# +# @author Chris Page <chris@starforge.co.uk> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +## @class Message::Queue +# This class allows messages to be added to the message queue, or retrieved from +# it in a format suitable for passing to Message::Sender. +# +# +package Message::Queue; +use strict; +use base qw(SystemModule); +use Utils qw(hash_or_hashref); + + +# ============================================================================ +# Constructor + +## @cmethod Message::Queue new(%args) +# Create a new Message::Queue object. This will create an Message::Queue object +# that may be used to store messages to send at a later date, retrieve those +# messages in a form that can be passed to Message::Sender::send_message(), or +# mark messages in the queue as deleted. +# +# @param args A hash of arguments to initialise the Message::Queue object with. +# @return A new Message::Queue object. +sub new { + my $invocant = shift; + my $class = ref($invocant) || $invocant; + + return $class -> SUPER::new(@_); +} + + +# ============================================================================ +# Addition and deletion + +## @method $ add_message($args) +# Add a message to the message queue. This will add a message to the queue table, +# ready to be sent at a later time by Message::Sender. The supported arguments are +# as follows: +# +# - subject (required) The email subject. +# - message (required) The body content to show in the email. +# - recipients (required) A reference to an array of userids. Each user will recieve +# a copy of the message. +# - unique_recip (optional) If set, copy of the message is made for each recipient, +# with one recipient per message (defaults to false). +# - ident (optional) Allow a user-definable identifier string to be attached to +# the message in the queue (if unique_recip is set, and more than one recipient +# is specified, the ident is set in each copy of the message). +# - userid (optional) Contains the ID of the user adding this message. If this is +# undef, the message is recorded as a system-generated one. Note that the +# interpretation of this field is controlled by Message::Sender - it may +# be used to determine the From: address, or it may be ignored. +# - send_at (optional) Specify the unix timestamp at which the message should be +# sent. If this is not specified, the creation time is used. +# - delay (optional) If specified, this introduces a delay, specified in seconds, +# between the message beng added and the first point at which it may be +# sent. Note that, if both this and send_at are specified, the delay is +# added to the value specified in send_at. +sub add_message { + my $self = shift; + my $args = hash_or_hashref(@_); + my $args -> {"now"} = time(); + + $self -> clear_error(); + + # Sort out the send time, based on possible user specified send time and delay + $args -> {"send_at"} = $args -> {"now"} unless($args -> {"send_at"}); + $args -> {"send_at"} += $args -> {"delay"} if($args -> {"delay"}); + + # FUTURE: potentially support other formats here. See also: https://www.youtube.com/watch?v=JENdgiAPD6c however. + my $format = "plain"; + + # Force required fields + return $self -> self_error("Email subject not specified") unless($args -> {"subject"}); + return $self -> self_error("Email body not specified") unless($args -> {"message"}); + return $self -> self_error("No recipients specified") + if(!$self -> {"recipients"} || ref($self -> {"recipients"}) ne "ARRAY" || !scalar(@{$self -> {"recipients"}})); + + # If unique recipients are set, each recipient gets a copy of the message + if($args -> {"unique_recip"}) { + foreach my $recip (@{$self -> {"recipients"}}) { + my $msgid = $self -> _queue_message($args) + or return undef; + + $self -> _add_recipient($msgid, $recip) + or return undef; + } + + # Otherwise there is one message with multiple recipients. + } else { + my $msgid = $self -> _queue_message($args) + or return undef; + + foreach my $recip (@{$self -> {"recipients"}}) { + $self -> _add_recipient($msgid, $recip) + or return undef; + } + } + + return 1; +} + + +## @method $ delete_message(%args) +# Delete a message from the queue. This will actually mark the message as deleted, +# messages are never really removed. Supported arguments are: +# +# - userid The ID of the user deleting the message. If undef, it is assumed the +# system is deleting the message. +# - id The ID of the message to delete. This will delete only this one message +# from the queue. +# - ident A message ident to search for and delete any messages that have it set. +# This allows a group of messages to be deleted in one go. +# +# @param args A hash, or reference to a hash, of arguments specifying the message +# to delete. +# @return The number of messages deleted on success (which maybe 0!), undef on error. +sub delete_message { + my $self = shift; + my $args = hash_or_hashref(@_); + my $now = time(); + + $self -> clear_error(); + + if($args -> {"id"}) { + return $self -> _delete_by_field("id", $args -> {"id"}, $args -> {"userid"}, $now); + } elsif($args -> {"ident"}) { + return $self -> _delete_by_field("ident", $args -> {"ident"}, $args -> {"userid"}, $now); + } + + return $self -> self_error("No id or ident passed to delete_message()"); +} + + +# ============================================================================ +# Ghastly internals + +## @method private $ _queue_message($args) +# Add a message row in the queue table. This creates a new message row, and +# returns its row ID if successful. +# +# @param args A reference to a hash containing the message data. +# @return The new message id on success, undef on error. +sub _queue_message { + my $self = shift; + my $args = shift; + + $self -> clear_error(); + + # Okay, give it a go... + my $newh = $self -> {"dbh"} -> prepare("INSERT INTO `".$self -> {"settings"} -> {"database"} -> {"message_queue"}."` + (created, creator_id, message_ident, subject, body, format, send_after) + VALUES(?, ?, ?, ?, ?, ?, ?)"); + my $result = $newh -> execute($args -> {"now"}, $args -> {"userid"}, $args -> {"ident"}, $args -> {"subject"}, $args -> {"message"}, $format, $args -> {"send_after"}); + return $self -> self_error("Unable to perform message insert: ". $self -> {"dbh"} -> errstr) if(!$result); + return $self -> self_error("Message insert failed, no rows inserted") if($result eq "0E0"); + + # FIXME: This ties to MySQL, but is more reliable that last_insert_id in general. + # Try to find a decent solution for this mess... + my $msgid = $self -> {"dbh"} -> {"mysql_insertid"} + or return $self -> self_error("Unable to obtain new message id"); + + return $msgid; +} + + +## @method private _add_recipient($messageid, $recipientid) +# Add a message recipient. This creates a new recipient row, associating the +# specified recipient userid with a message. +# +# @param messageid The ID of the message to add a recipient to. +# @param recipientid The ID of the user who should recieve the message. +# @return true on success, undef on error. +sub _add_recipient { + my $self = shift; + my $messageid = shift; + my $recipientid = shift; + + $self -> clear_error(); + + my $newh = $self -> {"dbh"} -> prepare("INSERT INTO `".$self -> {"settings"} -> {"database"} -> {"message_recipients"}."` + (message_id, recipient_id) + VALUES(?, ?)"); + my $result = $newh -> execute($messageid, $recipientid); + return $self -> self_error("Unable to perform recipient addition: ". $self -> {"dbh"} -> errstr) if(!$result); + return $self -> self_error("Recipient addition failed, no rows inserted") if($result eq "0E0"); + + return 1; +} + + +## @method private $ _delete_by_field($field, $value, $userid, $deleted) +# Attempt to delete messages where the specified field contains the value given. +# Note that this *does not* remove the message from the table, it simply marks +# it as deleted so that get_message() will not normally return it. +# +# @param field The database table field to search for messages on. +# @param value When a given message has this value in the specified field, it is +# marked as deleted (aleady deleted messages are not changed) +# @param userid The user performing the delete. May be undef. +# @param deleted The timestamp to place in the deleted field. +# @return The number of rows deleted. +sub _delete_by_field { + my $self = shift; + my $field = shift; + my $value = shift; + my $userid = shift; + my $deleted = shift; + + $self -> clear_error(); + + # Force valid field + $field = "id" unless($field eq "message_ident"); + + my $nukeh = $self -> {"dbh"} -> prepare("UPDATE `".$self -> {"settings"} -> {"database"} -> {"message_queue"}."` + SET deleted = ?, deleted_id = ? + WHERE $field = ? + AND deleted IS NULL"); + my $result = $nukeh -> execute($deleted, $userid, $value); + return $self -> self_error("Unable to perform message delete: ". $self -> {"dbh"} -> errstr) if(!$result); + + # Result should contain the number of rows updated. + return 0 if($result eq "0E0"); # need a special case for the zero rows, just in case... + return $result; +} + +1;