07-21-2005 17:29
This script is intended to provide a safe method to send e-mails without blocking your own script for extended periods or bringing down the wrath of the Lindens. See the comments in the header block of the script for usage instructions.

This is the first script I am releasing to the SL community, so am very interested to see how it goes. I am very interested in any feedback and test results that people can share. Will probably create an entry in the Scripting Library forum once I am convinced it works for other people, though the Wiki is probably a more useful archive.

WARNING: This script is currently in BETA testing. Please hold off if you are not comfortable modifying scripts or if you want to deploy it in any situation where you care if something goes wrong.

CODE

/////////////////////////////////////////////////////////////////
//
// MailQueue v0.0.1 -- a script for sending asynchronous e-mail
// Copyright (C) 2005, Christopher Wolfe
//
// This script is distributed under the Creative Commons
// Attribution-ShareAlike License as published by Creative Commons;
// either version 2.0 of the License, or (at your option) any
// later version. You may access a copy of the License via HTTP
// at <http://creativecommons.org/licenses/by-sa/2.0>.
//
// Invoking llMessageLinked calls to this script will not cause
// your script to be considered a derivative work.
//
// If you would like more information, have questions, or would like
// customizations or a different license please contact "Minsk Oud"
// via the Private Message features of the Second Life forums at
// <http://forums.secondlife.com/>
//
/////////////////////////////////////////////////////////////////
//
// This script handles queuing and dispatching e-mail messages
// in parallel to the execution of the script that created them.
// To avoid accidental flooding the length of the queue and the
// message rate can be controlled.
//
// Regardless of configuration, this script will never send an
// e-mail more than once every thirty seconds. Please DO NOT use
// multiple copies of this script to bypass that limit without
// explicit permission from Linden Labs.
//
// Link messages are filtered using a flexible protocol:
// The first line of the passed string must contain the same value
// as the name of the receiving script. The second line contains the
// method to execute. Later lines may be processed dependent on the
// method.
//
// For example, to call the SetRateBurst method (see below) on a
// copy of this script named MailQueue, you would use:
//
// // Set the burst rate to 5
// llMessageLinked(LINK_THIS, 5, "MailQueue\nSetRateBurst", NULL_KEY);
//
// More complex methods may require additional string inputs, like
// SendMessage:
//
// llMessageLinked(LINK_THIS, 0, "MailQueue\nSendMessage\n" +
// "the-recipient@example.com\n" +
// "The Subject Line Goes Here\n" +
// "Hello, the message here and\n" +
// "on following lines will be transmitted\n",
// NULL_KEY);
//
// The link messages supported by this script are:
//
// SendMessage
// line[2] = address
// line[3] = subject
// line[4...] = message
//
// The message will be silently discarded if either address or
// subject is empty.
//
// SetRateBurst
// integer = max emails in a burst (min=1)
//
// SetRateDelay
// integer = average seconds per email (min=30)
//
// SetQueueMaximum
// integer = maximum number of queued messages (min=1)
//
// SetTokensCount
// integer = number of messages that may be sent immediately (min=0)
//
// SetDebug
// integer = debug level (0 = none, 1,2,... = increasingly more)
//

// When set greater than zero, debugging information will be provided
// via llOwnerSay. Larger values will produce more copious information,
// so increment it gradually.
integer debug;

// The maximum number of messages that may be sent in a burst.
integer rate_burst;

// The average number of seconds required between messages.
integer rate_delay;

// The maximum number of messages in the queue. If this limit is exceeded,
// older messages will be dropped from the queue without being sent.
integer queue_maximum;

// The queue of messages to be sent. This is a 3-strided list,
// where each stride consists of:
// [ string address, string subject, string message ].
list queue;

// The current number of message tokens accumulated. This value is,
// logically, incremented every rate_delay seconds, and is no greater
// than rate_burst.
integer tokens_count;

// The GMT number of seconds after midnight at which the last token
// was granted.
float tokens_last;

// The time for which the next timer was scheduled. This is checked
// prior to scheduling a timer to ensure it is never increased.
float timer_next;

// Compute the minimum of two integers
integer IntMin(integer a, integer b)
{
if (a < b) return a;
else return b;
}

// Compute the maximum of two integers
integer IntMax(integer a, integer b)
{
if (a > b) return a;
else return b;
}

// Get lines starting with the nth line of a string
string StringGetLines(string str, integer n) {
for (; n > 0; --n) {
integer off = llSubStringIndex(str, "\n");
if (off == -1) return "";

str = llGetSubString(str, off + 1, -1);
}

return str;
}

// Get the nth line of a string
string StringGetLine(string str, integer n)
{
string rest = StringGetLines(str, n);
integer off = llSubStringIndex(rest, "\n");
if (off > 0) off -= 1;
return llGetSubString(rest, 0, off);
}

// Update the token count to reflect the current state
UpdateTokensCount()
{
float now = llGetGMTclock();

if (now < tokens_last) {
// The clock has wrapped across midnight. Correct tokens_last
// by making it a negative value:
tokens_last -= 24 * 60 * 60;
}

// Determine how many tokens have been granted
integer grant = (integer) ((now - tokens_last) / rate_delay);

// Increase tokens_last to reflect the new token allocation
tokens_last += rate_delay * grant;

// Update the token count appropriately
tokens_count = IntMin(tokens_count + grant, rate_burst);

if (debug >= 3) llOwnerSay(llGetObjectName() + "." + llGetScriptName() +
" updated token count to " + (string) tokens_count);
}

// Update the scheduled timer based on the current state
UpdateTimer()
{
float delay;

// Check to see if timer should be updated
if (tokens_count > 0) {
if (llGetListLength(queue) > 0) {
// Tokens are available and messages are pending, activate
delay = 10;

}
else {
// Tokens are available and no messages are pending, sleep
delay = 60 * 60;
}
}
else {
if (llGetListLength(queue) > 0) {
// No tokens are available, but messages are pending.
// Schedule the timer for ten seconds after a token will be
// available (to avoid time slew problems). Ensure that this
// is never less than ten seconds in the future.

delay = IntMax((integer)
(tokens_last + rate_delay + 10 - llGetGMTclock()), 10);
}
else {
// No tokens are available, but none are needed, Sleep
delay = 60 * 60;
}
}

float now = llGetGMTclock();
if (timer_next < 0 || now + delay < timer_next) {
if (debug >= 3) llOwnerSay(llGetObjectName() + "." + llGetScriptName() +
" scheduled timer delay of " + (string) delay);

timer_next = llGetGMTclock() + delay;
llSetTimerEvent(delay);
}
}

default
{
state_entry()
{
debug = 0;

// The script has been reset. Ensure that all values have
// paranoid defaults configured.

// These defaults permit:
// four messages to be sent immediately,
// or eight over an hour
//
// Please use the link message operations to configure your queue
// rather than modifying these values for each configuration.

rate_burst = 4; // burst of four messages
rate_delay = 300; // every 5 minutes
queue_maximum = 4; // maximum four messages in the queue

queue = [];

tokens_count = rate_burst;
tokens_last = llGetGMTclock();

timer_next = -1;
UpdateTimer();
}

link_message(integer from, integer num, string str, key id)
{
// TODO: Should optimize the line parsing to avoid repeated searches.
// Values are short enough that it is not a high priority.

string recipient = StringGetLine(str, 0);
if (recipient == llGetScriptName()) {
// Ensure the token count is updated before doing anything
UpdateTokensCount();

string command = StringGetLine(str, 1);

if (debug >= 3) llOwnerSay(llGetObjectName() + "." + llGetScriptName() +
" received link message, command = " + command);

if (command == "SendMessage") {

string address = StringGetLine(str, 2);
string subject = StringGetLine(str, 3);
string message = StringGetLines(str, 4);

// Abort if either the address or the subject is empty
if (address != "" && subject != "") {

if (llGetListLength(queue) / 3 >= queue_maximum) {
integer drop = llGetListLength(queue) / 3 - queue_maximum;

// Adding this message would overflow the queue, remove
// the oldest entries as necessary.
queue = llDeleteSubList(queue, 0, drop);

if (debug >= 2)
llOwnerSay(llGetObjectName() + "." + llGetScriptName() +
" dropped " + (string) drop + " messages");

}

if (debug >= 2)
llOwnerSay(llGetObjectName() + "." + llGetScriptName() +
" enqueued message to " + address + ": " + subject);

// Finally, enqueue the message
queue = queue + [ address, subject, message ];
}
}
else if (command == "SetRateBurst") {
rate_burst = IntMax(num, 1);
}
else if (command == "SetRateDelay") {
rate_delay = IntMax(num, 30);
}
else if (command == "SetQueueMaximum") {
queue_maximum = IntMax(num, 1);
}
else if (command == "SetTokensCount") {
tokens_count = IntMax(num, 0);
tokens_last = llGetGMTclock();
}
else if (command == "SetDebug") {
if (debug >= 1 || num >= 1)
llOwnerSay(llGetObjectName() + "." + llGetScriptName() +
" setting debug level to " + (string) num);
debug = num;
}

UpdateTimer();
}
}

timer()
{
UpdateTokensCount();

// There are available tokens and messages need to be sent
if (tokens_count > 0 && llGetListLength(queue) > 0) {
tokens_count -= 1;

string address = llList2String(queue, 0);
string subject = llList2String(queue, 1);
string message = llList2String(queue, 2);
queue = llDeleteSubList(queue, 0, 2);

if (debug >= 2) llOwnerSay(llGetObjectName() + "." + llGetScriptName() +
" sent message to " + address + ": " + subject + " --- " + message);
else if (debug >= 1) llOwnerSay(llGetObjectName() + "." + llGetScriptName() +
" sent message to " + address + ": " + subject);

llEmail(address, subject, message);
}

timer_next = -1;
UpdateTimer();
}
}