1
0
mirror of https://github.com/livingcomputermuseum/IFS.git synced 2026-05-05 23:44:09 +00:00

Added basic mail support. No routing.

This commit is contained in:
Josh Dersch
2016-06-06 14:57:17 -07:00
parent 0a8b83c315
commit c03d930bec
22 changed files with 806 additions and 198 deletions

View File

@@ -7,11 +7,13 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.IO;
using IFS.Mail;
namespace IFS.FTP
{
public enum FTPCommand
{
// Standard FTP
Invalid = 0,
Retrieve = 1,
Store = 2,
@@ -27,10 +29,17 @@ namespace IFS.FTP
NewEnumerate = 12,
Delete = 14,
Rename = 15,
// Mail-specific
StoreMail = 16,
RetrieveMail = 17,
FlushMailbox = 18,
MailboxException = 19,
}
public enum NoCode
{
// Standard FTP
UnimplmentedCommand = 1,
UserNameRequired = 2,
IllegalCommand = 3,
@@ -64,7 +73,12 @@ namespace IFS.FTP
TransientServerFailure = 71,
PermamentServerFailure = 72,
FileBusy = 73,
FileAlreadyExists = 74
FileAlreadyExists = 74,
// Mail-specific
NoValidMailbox = 32,
IllegalMailboxSyntax = 33,
IllegalSender = 34,
}
struct FTPYesNoVersion
@@ -210,7 +224,7 @@ namespace IFS.FTP
// Argument to New-Store is a property list (string).
//
string fileSpec = Helpers.ArrayToString(data);
Log.Write(LogType.Verbose, LogComponent.FTP, "File spec for new-store is '{0}'.", fileSpec);
Log.Write(LogType.Verbose, LogComponent.FTP, "File spec for delete is '{0}'.", fileSpec);
PropertyList pl = new PropertyList(fileSpec);
@@ -218,6 +232,61 @@ namespace IFS.FTP
}
break;
case FTPCommand.RetrieveMail:
{
// Argument to Retrieve-Mail is a property list (string).
//
string mailSpec = Helpers.ArrayToString(data);
Log.Write(LogType.Verbose, LogComponent.FTP, "Mailbox spec for retrieve-mail is '{0}'.", mailSpec);
PropertyList pl = new PropertyList(mailSpec);
RetrieveMail(pl);
}
break;
case FTPCommand.FlushMailbox:
{
if (_lastRetrievedMailFiles != null)
{
foreach (string mailFileToDelete in _lastRetrievedMailFiles)
{
MailManager.DeleteMail(_lastRetrievedMailbox, mailFileToDelete);
}
}
SendFTPYesResponse("Retreived mail flushed from mailbox.");
}
break;
case FTPCommand.StoreMail:
{
//
// Argument to Retrieve-Mail is one or more property lists.
//
string mailSpec = Helpers.ArrayToString(data);
Log.Write(LogType.Verbose, LogComponent.FTP, "Mailbox spec for store-mail is '{0}'.", mailSpec);
//
// Annoyingly, the argument is numerous property lists with no delimiter, not a single list.
//
List<PropertyList> recipients = new List<PropertyList>();
int currentIndex = 0;
while (currentIndex < mailSpec.Length)
{
int endIndex = 0;
PropertyList pl = new PropertyList(mailSpec, currentIndex, out endIndex);
recipients.Add(pl);
currentIndex = endIndex;
}
StoreMail(recipients);
}
break;
default:
Log.Write(LogType.Warning, LogComponent.FTP, "Unhandled FTP command {0}.", command);
break;
@@ -631,6 +700,243 @@ namespace IFS.FTP
_channel.SendMark((byte)FTPCommand.EndOfCommand, true);
}
/// <summary>
/// Retrieves mail files for the specified mailbox.
/// </summary>
/// <param name="fileSpec"></param>
private void RetrieveMail(PropertyList mailSpec)
{
//
// The property list must include a Mailbox property that identifies the mailbox to be opened.
//
if (!mailSpec.ContainsPropertyValue(KnownPropertyNames.Mailbox))
{
SendFTPNoResponse(NoCode.NoValidMailbox, "No mailbox specified.");
return;
}
_lastRetrievedMailbox = mailSpec.GetPropertyValue(KnownPropertyNames.Mailbox);
//
// Validate that the requested mailbox's registry is on this server.
//
if (!Authentication.ValidateUserRegistry(_lastRetrievedMailbox))
{
SendFTPNoResponse(NoCode.NoValidMailbox, "Incorrect registry for this server.");
return;
}
_lastRetrievedMailbox = Authentication.GetUserNameFromFullName(_lastRetrievedMailbox);
//
// Authenticate and see if the user has access to the requested mailbox.
// In our implementation, the username and mailbox name are one and the same, if the two don't match
// (or if authentication failed) then we're done.
//
UserToken user = AuthenticateUser(mailSpec);
if (user == null)
{
SendFTPNoResponse(NoCode.IllegalConnectPassword, "Invalid username or password for mailbox.");
return;
}
if (user.UserName.ToLowerInvariant() != _lastRetrievedMailbox.ToLowerInvariant())
{
SendFTPNoResponse(NoCode.AccessDenied, "You do not have access to the specified mailbox.");
return;
}
//
// All clear at this point. If the user has any mail in his/her mailbox, send it now.
//
_lastRetrievedMailFiles = MailManager.EnumerateMail(_lastRetrievedMailbox);
if (_lastRetrievedMailFiles != null)
{
foreach (string mailFile in _lastRetrievedMailFiles)
{
using (Stream mailStream = MailManager.RetrieveMail(_lastRetrievedMailbox, mailFile))
{
Log.Write(LogType.Verbose, LogComponent.FTP, "Preparing to send mail file {0}.", mailFile);
//
// Build a property list for the mail message.
//
PropertyList mailProps = new PropertyList();
mailProps.SetPropertyValue(KnownPropertyNames.Length, mailStream.Length.ToString());
mailProps.SetPropertyValue(KnownPropertyNames.DateReceived, MailManager.GetReceivedTime(_lastRetrievedMailbox, mailFile));
mailProps.SetPropertyValue(KnownPropertyNames.Opened, "No");
mailProps.SetPropertyValue(KnownPropertyNames.Deleted, "No");
mailProps.SetPropertyValue(KnownPropertyNames.Type, "Text"); // We treat all mail as text
mailProps.SetPropertyValue(KnownPropertyNames.ByteSize, "8"); // 8-bit bytes, please.
//
// Send the property list (without EOC)
//
_channel.SendMark((byte)FTPCommand.HereIsPropertyList, false);
_channel.Send(Helpers.StringToArray(mailProps.ToString()));
//
// Send the mail text.
//
_channel.SendMark((byte)FTPCommand.HereIsFile, true);
byte[] data = new byte[512];
while (true)
{
int read = mailStream.Read(data, 0, data.Length);
if (read == 0)
{
// Nothing to send, we're done.
break;
}
Log.Write(LogType.Verbose, LogComponent.FTP, "Sending mail data, current file position {0}.", mailStream.Position);
_channel.Send(data, read, true);
if (read < data.Length)
{
// Short read, end of file.
break;
}
}
Log.Write(LogType.Verbose, LogComponent.FTP, "Mail file {0} sent.", mailFile);
}
}
}
// All done, send a Yes/EOC to terminate the exchange.
SendFTPYesResponse("Mail retrieved.");
}
/// <summary>
/// Stores mail files to the specified mailboxes.
/// </summary>
/// <param name="fileSpec"></param>
private void StoreMail(List<PropertyList> mailSpecs)
{
//
// There are two defined properties: Mailbox and Sender.
// The Sender field can really be anything and real IFS servers
// did not authenticate or verify this name. We will simply ignore
// it since we can't really do anything useful with it.
// (I do like the total lack of security in this protocol.)
//
List<string> destinationMailboxes = new List<string>();
foreach (PropertyList mailSpec in mailSpecs)
{
if (!mailSpec.ContainsPropertyValue(KnownPropertyNames.Mailbox))
{
Log.Write(LogType.Verbose, LogComponent.Mail, "No mailbox specified, aborting.");
SendFTPNoResponse(NoCode.NoValidMailbox, "No mailbox specified.");
return;
}
string destinationMailbox = mailSpec.GetPropertyValue(KnownPropertyNames.Mailbox);
//
// Validate that the destination mailbox's registry is on this server.
// We do not support forwarding or routing of mail, so mail intended for another server
// will never get there.
//
if (!Authentication.ValidateUserRegistry(destinationMailbox))
{
SendFTPNoResponse(NoCode.NoValidMailbox, "Incorrect registry for this server. Mail forwarding not supported.");
return;
}
destinationMailbox = Authentication.GetUserNameFromFullName(destinationMailbox);
// Verify that the user we're sending this to actually exists...
if (!Authentication.UserExists(destinationMailbox))
{
Log.Write(LogType.Verbose, LogComponent.Mail, "Mailbox {0} does not exist, aborting.", destinationMailbox);
SendFTPNoResponse(NoCode.NoValidMailbox, "The specified mailbox does not exist.");
return;
}
destinationMailboxes.Add(destinationMailbox);
}
// OK so far, send a Yes and wait for a file.
SendFTPYesResponse("Go ahead.");
//
// We now expect a "Here-Is-File"...
//
FTPCommand hereIsFile = (FTPCommand)_channel.WaitForMark();
if (hereIsFile != FTPCommand.HereIsFile)
{
throw new InvalidOperationException("Expected Here-Is-File from client.");
}
//
// At this point the client should start sending data, so we should start receiving it.
//
bool success = true;
FTPCommand lastMark;
byte[] buffer;
try
{
Log.Write(LogType.Verbose, LogComponent.Mail, "Receiving mail file to memory...");
// TODO: move to constant. Possibly make max size configurable.
// For now, it seems very unlikely that any Alto is going to have a single file larger than 4mb.
lastMark = ReadUntilNextMark(out buffer, 4096 * 1024);
Log.Write(LogType.Verbose, LogComponent.Mail, "Received {0} bytes.", buffer.Length);
// Write out to files
foreach (string destination in destinationMailboxes)
{
using (Stream mailFile = MailManager.StoreMail(destination))
{
mailFile.Write(buffer, 0, buffer.Length);
Log.Write(LogType.Verbose, LogComponent.Mail, "Wrote {0} bytes to mail file in mailbox {1}.", buffer.Length, destination);
}
}
}
catch (Exception e)
{
// We failed while writing the mail file, send a No response to the client.
// Per the spec, we need to drain the client data first.
lastMark = ReadUntilNextMark(out buffer, 4096 * 1024); // TODO: move to constant
success = false;
Log.Write(LogType.Warning, LogComponent.Mail, "Failed to write mail file. Error '{1}'. Aborting.", e.Message);
}
// Read in the last command we got (should be a Yes or No). This is sort of annoying in that it breaks the normal convention of
// Command followed by EndOfCommand, so we have to read the remainder of the Yes/No command separately.
if (lastMark != FTPCommand.Yes && lastMark != FTPCommand.No)
{
throw new InvalidOperationException("Expected Yes or No response from client after transfer.");
}
buffer = ReadNextCommandData();
FTPYesNoVersion clientYesNo = (FTPYesNoVersion)Serializer.Deserialize(buffer, typeof(FTPYesNoVersion));
Log.Write(LogType.Verbose, LogComponent.Mail, "Client success code is {0}, {1}, '{2}'", lastMark, clientYesNo.Code, clientYesNo.Code);
if (!success)
{
// TODO: provide actual No codes.
SendFTPNoResponse(NoCode.TransientServerFailure, "Mail transfer failed.");
}
else
{
SendFTPYesResponse("Mail transfer completed.");
}
}
/// <summary>
/// Open the file specified by the provided PropertyList
/// </summary>
@@ -818,8 +1124,8 @@ namespace IFS.FTP
{
password = fileSpec.GetPropertyValue(KnownPropertyNames.UserPassword);
}
UserToken user = Authentication.Authenticate(userName, password);
UserToken user = Authentication.Authenticate(userName, password);
if (user == null)
{
@@ -847,7 +1153,7 @@ namespace IFS.FTP
string userDirPath = Path.Combine(Configuration.FTPRoot, userToken.HomeDirectory);
return fullPath.StartsWith(userDirPath, StringComparison.OrdinalIgnoreCase);
}
}
private void SendFTPResponse(FTPCommand responseCommand, object data)
{
@@ -896,5 +1202,13 @@ namespace IFS.FTP
private Thread _workerThread;
private bool _running;
/// <summary>
/// The last set of mail files retrieved via a Retrieve-Mail operation.
/// Saved so that Flush-Mailbox can use it to delete only those mails that
/// were last pulled.
/// </summary>
private IEnumerable<string> _lastRetrievedMailFiles;
private string _lastRetrievedMailbox;
}
}

View File

@@ -34,6 +34,13 @@ namespace IFS.FTP
public static readonly string Author = "Author";
public static readonly string Checksum = "Checksum";
public static readonly string DesiredProperty = "Desired-Property";
// Mail
public static readonly string Mailbox = "Mailbox";
public static readonly string Length = "Length";
public static readonly string DateReceived = "Date-Received";
public static readonly string Opened = "Opened";
public static readonly string Deleted = "Deleted";
}
/// <summary>
@@ -76,9 +83,25 @@ namespace IFS.FTP
_propertyList = new Dictionary<string, string>();
}
/// <summary>
/// Parses a property list from the specified string.
/// </summary>
/// <param name="list"></param>
public PropertyList(string list) : this()
{
ParseList(list);
ParseList(list, 0);
}
/// <summary>
/// Parses a property list from the specified string at the given starting offset.
/// endIndex returns the end of the parsed property list in the string.
/// </summary>
/// <param name="input"></param>
/// <param name="startIndex"></param>
/// <param name="endIndex"></param>
public PropertyList(string input, int startIndex, out int endIndex) : this()
{
endIndex = ParseList(input, startIndex);
}
/// <summary>
@@ -171,8 +194,10 @@ namespace IFS.FTP
/// Parses a string representation of a property list into our hash table.
/// </summary>
/// <param name="list"></param>
private void ParseList(string list)
private int ParseList(string input, int startOffset)
{
string list = input.Substring(startOffset);
//
// First check the basics; the string must start and end with left and right parens, respectively.
// We do not trim whitespace as there should not be any per the spec.
@@ -191,8 +216,14 @@ namespace IFS.FTP
//
// Loop until we hit the end of the string (minus the closing paren)
//
while (index < list.Length - 1)
while (index < list.Length)
{
// If this is a closing paren, this denotes the end of the property list.
if (list[index] == ')')
{
break;
}
// Start of next property, must begin with a left paren.
if (list[index] != '(')
{
@@ -273,6 +304,8 @@ namespace IFS.FTP
throw new InvalidOperationException(String.Format("Duplicate property entry for '{0}", propertyName));
}
}
return index + startOffset + 1;
}
private Dictionary<string, string> _propertyList;