Monday, November 08, 2010 at 12:06 AM.

system.verbs.builtins.tcp.sendMail

on sendMail (recipient="", subject="", message="", sender=user.prefs.mailAddress, cc="", bcc="", host=user.prefs.mailHost, mimeType="text/plain", adrHdrTable=nil, timeOutTicks=60*60,  flMessages=true) {
	
	<<Changes
		<<Wed, 16 Jun 1999 00:05:16 GMT by AR
			<<Added optional adrHdrTable parameter, the address of a table. If an address is specified, its fields are added to the message header, possibly overriding standard header items.
			<<Added optional timeOutTicks parameter. The default value is 3600 ticks (60 seconds). If the connection is idle for more than the specified number of ticks while waiting for a response from the server, the connection is closed and an error message is generated.
			<<Added optional flMessages parameter. If it is true (the default), status messages will be displayed in Frontier's About window. The displayed messages are prefixed with the name of this script. Before we return, we clean up the message area.
			<<Added extensive error checking and reporting code. If the script returns true, it is now safe to assume that the message was accepted by the server. Status messages received from the server indicating that an error has occured are no longer ignored. Whenever an error occurs, we try to shut down the connection gracefully before throwing a scripterror.
			<<Calls to thread.sleepFor were added to the TCP read/write loops for improved thread-friendliness.
		<<7/25/99; 8:04:26 AM by DW
			<<Loop over scripts in user.callbacks.sendmail table.
		<<11/05/1999 at 12:57:30 AM by AR
			<<Updated to use 6.1 TCP verbs.
			<<Cleaned up low-level error messages to conform to "Can't do x because y" scheme.
	
	bundle { //loop over callbacks
		if defined (user.callbacks.tcpSendMail) {
			local (i);
			for i = 1 to sizeof (user.callbacks.tcpSendMail) {
				try {
					local (adrcallback = @user.callbacks.tcpSendMail [i]);
					if typeof (adrcallback^) == scriptType { //avoid problems with item #1s
						if adrcallback^ (recipient, subject, message, sender, cc, bcc, mimetype) {
							<<callback returns true if it consumed the sendMail operation
							return (true)}}}}}};
	
	on displayMsg (s) {
		msg ("tcp.sendMail: " + s)};
	
	local (buffer = "", response = "");
	on add (s) { //appends crlf delimited items to our message buffer
		buffer = buffer + s + cr + lf};
	
	bundle { //build the header and store in buffer
		local (addedFromTableList = {});
		on addFromHeaderTable (s) {
			if (adrHdrTable != nil) and defined (adrHdrTable^.[s]) {
				add (nameOf (adrHdrTable^.[s]) + ": " + adrHdrTable^.[s]);
				addedFromTableList = addedFromTableList + string.lower (s);
				return (true)};
			return (false)};
		if not addFromHeaderTable ("X-Mailer") {
			add ("X-Mailer: " + "UserLand Frontier " + Frontier.version () + " (" + sys.osName () + ")")};
		if not addFromHeaderTable ("Mime-Version") {
			add ("Mime-Version: 1.0")};
		if not addFromHeaderTable ("Content-Type") {
			add ("Content-Type: " + mimeType + "; charset=\"us-ascii\"")};
		if not addFromHeaderTable ("Date") {
			add ("Date: " +  date.netStandardString (clock.now ()))};
		if not addFromHeaderTable ("To") {
			if  sizeOf (recipient) > 0 {
				add ("To: " + recipient)}};
		if not addFromHeaderTable ("CC") {
			if sizeOf (cc) > 0 {
				add ("CC: " + cc)}};
		if not addFromHeaderTable ("From") {
			add ("From: " + sender)};
		if not addFromHeaderTable ("Subject") {
			add ("Subject: " + subject)};
		if adrHdrTable != nil { //now add remaining header fields from the table
			local (i);
			for i = 1 to sizeOf (adrHdrTable^) {
				if not (addedFromTableList contains string.lower (nameOf (adrHdrTable^ [i]))) {
					add (nameOf (adrHdrTable^ [i]) + ": " + adrHdrTable^ [i])}}};
		add ("")}; //a blank line before the content is required!
	
	bundle { //build message body and store in buffer
		if not (message contains lf) { //convert line-endings
			message = string.replaceAll (message, cr, cr+lf)};
		message = string.replaceAll (message, "\r\n.\r\n", "\r\n..\r\n"); //replace lines beginning with a period with two periods
		if message beginsWith ".\r\n" { //if the first line begins with a period
			message = "." + message};
		if not (message endsWith "\r\n") {
			message = message + "\r\n"};
		add (message)}; //add our message text to the buffer
	
	on sendCommandToServer (stream, command) { //AR 10/27/1999
		<<on sendCommandToServer (stream, command) //pre-6.1 code
			<<local (chunksize = 4 * 1024)
			<<local (lastWrite = clock.ticks ())
			<<loop //write command to the stream
				<<try
					<<if sizeof (command) <= chunkSize
						<<tcp.writeStream (stream, command)
						<<sys.systemTask()
						<<thread.sleepfor (0)
						<<break
					<<tcp.writeStream (stream, string.mid (command, 1, chunkSize))
					<<command = string.delete (command, 1, chunkSize)
					<<sys.systemTask()
					<<thread.sleepfor (0)
					<<lastWrite = clock.ticks ()
				<<else
					<<if (tryError contains "10035") and (clock.ticks () < (lastWrite + (60 * 10))) //10035 means the socket buffers are not ready, give them up to 10 seconds.
						<<sys.systemTask()
						<<thread.sleepfor (1) //yield this thread for a second to allow normal frontier processing
					<<else
						<<try {tcp.closeStream (stream)}
						<<scriptError ("Error communicating with SMTP host: " + tryError) // re-throw the error
			<<return (true)
		try {
			tcp.writeStringToStream (stream, command, 4096, timeOutTicks / 60)}
		else {
			if flMessages { //clean up the message area
				msg ("")};
			try {tcp.abortStream (stream)};
			scriptError ("Can't send mail because a connection error occured: " + tryerror)};
		return (true)};
	on receiveServerResponse (stream) { //AR 10/27/1999
		<<on receiveServerResponse (stream) //pre-6.1 code
			<<local (response = "", bytesPending = 0, ix, linetext)
			<<local (timeout = clock.ticks () + timeOutTicks)
			<<loop //read data from the stream
				<<case tcp.statusStream (stream, @bytespending)
					<<"DATA"  //read data
						<<response = response + tcp.readStream (stream, bytespending)
						<<ix = string.patternMatch ("\r\n", response)
						<<while (ix > 0)  //handle multi-line responses
							<<linetext = string.mid (response, 1, ix-1)
							<<if string.nthChar (linetext, 4) == ' ' //the final line of the response has a space as the fourth char
								<<return (linetext)
							<<response = string.delete (response, 1, ix+1)
							<<ix = string.patternMatch ("\r\n", response)
						<<sys.systemTask()
						<<thread.sleepfor (0)
						<<timeout = clock.ticks () + timeOutTicks
						<<continue
					<<"OPEN"  //waiting for data
						<<if clock.ticks () > timeout
							<<scriptError ("Can't send mail because the connection to " + host + " timed out.")
						<<sys.systemTask()
						<<thread.sleepfor (0)
						<<continue
				<<else //connection terminated prematurely
					<<try {tcp.closeStream (stream)}
					<<scriptError ("Can't send mail because the connection to " + host + " was terminated prematurely.")
			<<return ("") //this should never be executed, either we return the server's final response line or we throw a scripterror
		local (ix, linetext);
		loop { //read data from the stream
			try {
				tcp.readStreamUntil (stream, "\r\n", timeOutTicks / 60, @response)}
			else {
				try {tcp.abortStream (stream)};
				if flMessages { //clean up the message area
					msg ("")};
				scriptError ("Can't send mail because a connection error occured: " + tryerror)};
			ix = string.patternMatch ("\r\n", response);
			while (ix > 0) { //handle multi-line responses
				linetext = string.mid (response, 1, ix-1);
				response = string.delete (response, 1, ix+1);
				if string.nthChar (linetext, 4) == ' ' { //the final line of the response has a space as the fourth char
					return (linetext)};
				ix = string.patternMatch ("\r\n", response)}};
		return ("")}; //this should never be executed, either we return the server's final response line or we throw a scripterror
	on executeCommand (command, expectResponse = nil) { //send a command to the server and handle the response
		local (statusline);
		sendCommandToServer (stream, command + "\r\n");
		statusline = receiveServerResponse (stream);
		if flMessages {
			displayMsg (statusline)};
		if (expectResponse != nil) and not (statusline beginsWith expectResponse) {
			closeConnection ("Can't send mail because of an unexpected server response. I said \"" + command + "\" and the server replied \"" + statusline + "\".")};
		return (true)};
	on addRecipient (recipient) { //send recipient command
		executeCommand ("RCPT TO: <" + string.cleanMailAddress (recipient) + ">", "25")}; //expect status code 250 or 251
	on closeConnection (errorstring = "") { //also called in case of unexpected server responses
		try {executeCommand ("RSET")}; //don't expect a specific response, otherwise we might end up in an infinite loop
		try {executeCommand ("QUIT")};
		try {tcp.closeStream (stream)};
		if flMessages { //clean up the message area
			msg ("")};
		if errorstring != "" { //possibly (re)throw scripterrors
			scriptError (errorstring)}};
	
	local (stream);
	
	try { //try to open connection to server
		stream = tcp.openStream (host, 25)}
	else { // fatal error
		scriptError ("Can't send mail because the connection to " + host + " could not be opened: " + tryError)};
	
	bundle { //wait for server's initial connection greeting
		local (statusline);
		statusline = receiveServerResponse (stream);
		if flMessages {
			displayMsg (statusline)};
		if not (statusline beginsWith "220") {
			closeConnection ("Can't send mail because the server, " + host + ", did not send a valid connection greeting. It said, \"" + statusline + "\".")}};
	
	bundle { //say hello and reset state
		executeCommand ("HELO " + tcp.addressDecode (tcp.myAddress()), "250");
		executeCommand ("RSET", "250")};
	
	bundle { //specify sender and recipients
		executeCommand ("MAIL FROM: <" + string.cleanMailAddress (sender) + ">", "250");
		local (i);
		if sizeOf (recipient) > 0 {
			for i = 1 to string.countFields (recipient, ',') {
				addRecipient (string.nthField (recipient, ',', i))}};
		if sizeOf (cc) > 0 {
			for i = 1 to string.countFields (cc, ',') {
				addRecipient (string.nthField (cc, ',', i))}};
		if sizeOf (bcc) > 0 {
			for i = 1 to string.countFields (bcc, ',') {
				addRecipient (string.nthField (bcc, ',', i))}}};
	
	bundle { //send message data
		executeCommand ("DATA", "354");
		sendCommandToServer (stream, buffer);
		executeCommand (".", "250")};
	
	closeConnection ();
	
	return (true)}



This listing is for code that runs in the OPML Editor environment. I created these listings because I wanted the search engines to index it, so that when I want to look up something in my codebase I don't have to use the much slower search functionality in my object database. Dave Winer.