unit WideIdEMailAddress;

interface

uses
  Classes, TntClasses;

type
  TWideIdEMailAddressItem = class(TCollectionItem)
  protected
    FAddress: Widestring;
    FName: Widestring;
    function GetText: Widestring;
    procedure SetText(AText: Widestring);
    function ConvertAddress: Widestring;
    function GetDomain: Widestring;
    procedure SetDomain(const ADomain: Widestring);
    function GetUsername: Widestring;
    procedure SetUsername(const AUsername: Widestring);
  public
    procedure Assign(Source: TPersistent); override;
  published
     {This is the E-Mail address itself }
    property Address: Widestring read FAddress write FAddress;
     {This is the person's name}{Do not Localize}
    property Name: Widestring read FName write FName;
     {This is the combined person's name and E-Mail address}{Do not Localize}
    property Text: Widestring read GetText write SetText;
     {Extracted domain for some types of E-Mail processing}
    property Domain: Widestring read GetDomain write SetDomain;
    property User: Widestring read GetUsername write SetUsername;
  end;

  TWideIdEMailAddressList = class(TOwnedCollection)
  protected
    function GetItem(Index: Integer): TWideIdEMailAddressItem;
    procedure SetItem(Index: Integer; const Value: TWideIdEMailAddressItem);
    function GetEMailAddresses: Widestring;
    procedure SetEMailAddresses(AList: Widestring);
  public
    constructor Create(AOwner: TPersistent); reintroduce;

     {This returns formatted list of formated
     addresses including the names from the collection }
    procedure FillTStrings(AStrings: TTntStrings);
    function Add: TWideIdEMailAddressItem;
     //get all of the domains in the list so we can process those individually with
     //TIdDirectSMTP
    procedure GetDomains(AStrings: TTntStrings);
     {Sort by domains for making it easier to process E-Mails directly in
     TIdDirectSMTP}
    procedure SortByDomain;
     //Get all of the E-Mail addresses for a particular domain so we can
     //send E-Mail to recipients at one domain with only one connection for
     //speed with TIdDirectSMTP.
    procedure AddressesByDomain(AList: TWideIdEMailAddressList; const ADomain: Widestring);
    property Items[Index: Integer]: TWideIdEMailAddressItem read GetItem write SetItem; default;
     {This is a comma separated list of formated
     addresses including the names from the collection }
    property EMailAddresses: Widestring read GetEMailAddresses write SetEMailAddresses;
  end;

const
  WideTab: widechar = #9;

implementation

uses
  SysUtils, TntSysUtils;

const
  // This is actually the ATEXT without the '"' and space characters...    {Do not Localize}
  IETF_ATEXT: Widestring = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' {Do not Localize}
  + '1234567890!#$%&''*+-/=?_`{}|~'; {Do not Localize}
  // ATEXT without the '"'    {Do not Localize}
  IETF_ATEXT_SPACE: Widestring = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' {Do not Localize}
  + '1234567890!#$%&''*+-/=?_`{}|~ '; {Do not Localize}
  IETF_QUOTABLE: Widestring = '\"'; {Do not Localize}

// Three functions for easier manipulating of strings.
// Don't know of any system functions to perform these actions.    {Do not Localize}
// If there aren't & someone can find an optimised way of performing    {Do not Localize}
// then please implement...

function FindFirstOf(const AFind, AText: Widestring): Integer;
var
  nCount, nPos: Integer;
begin
  Result := 0;
  for nCount := 1 to Length(AFind) do begin
    nPos := Pos(AFind[nCount], AText);
    if nPos > 0 then begin
      if Result = 0 then begin
        Result := nPos;
      end else if Result > nPos then begin
        Result := nPos;
      end;
    end;
  end;
end;

function FindFirstNotOf(const AFind, AText: Widestring): Integer;
var
  i: Integer;
begin
  Result := 0;
  if AFind = '' then
  begin
    Result := 1;
    Exit;
  end;

  if AText = '' then
  begin
    Exit;
  end;

  for i := 1 to Length(AText) do
  begin
    if Pos(AText[i], AFind) = 0 then
    begin
      Result := i;
      Exit;
    end;
  end;
end;

function TrimAllOf(const ATrim, AText: Widestring): Widestring;
var
  Len: Integer;
begin
  Result := AText;
  Len := Length(Result);
  while Len > 0 do
  begin
    if Pos(Result[1], ATrim) > 0 then
    begin
      Delete(Result, 1, 1);
      Dec(Len);
    end else Break;
  end;
  while Len > 0 do begin
    if Pos(Result[Len], ATrim) > 0 then
    begin
      Delete(Result, Len, 1);
      Dec(Len);
    end else Break;
  end;
end;

{ TWideIdEMailAddressItem }

procedure TWideIdEMailAddressItem.Assign(Source: TPersistent);
var Addr: TWideIdEMailAddressItem;
begin
  if ClassType <> Source.ClassType then
  begin
    inherited
  end
  else
  begin
    Addr := TWideIdEMailAddressItem(Source);
    Address := Addr.Address;
    Name := Addr.Name;
  end;
end;

function TWideIdEMailAddressItem.ConvertAddress: Widestring;
var
  i: Integer;
  domainPart, tempAddress, localPart: Widestring;
begin
  if FAddress = '' then
  begin
    if FName <> '' then
    begin
      Result := '<>'; {Do not Localize}
    end else
    begin
      Result := ''; {Do not Localize}
    end;
    Exit;
  end;

  // First work backwards to the @ sign.
  tempAddress := FAddress;
  domainPart := '';
  for i := Length(FAddress) downto 1 do
  begin
    if FAddress[i] = '@' then {Do not Localize}
    begin
      domainPart := Copy(FAddress, i, MaxInt);
      tempAddress := Copy(FAddress, 1, i - 1);
      Break;
    end;
  end;

  i := FindFirstNotOf(IETF_ATEXT, tempAddress);
  if (i = 0) or (Copy(tempAddress, i, 1) = #46) then //hack to accomodate periods in emailaddress
//  if i = 0 then
  begin
    if FName <> '' then
    begin
      Result := '<' + tempAddress + domainPart + '>'; {Do not Localize}
    end else
    begin
      Result := tempAddress + domainPart;
    end;
  end else
  begin
    localPart := '"'; {Do not Localize}
    while i > 0 do
    begin
      localPart := localPart + Copy(tempAddress, 1, i - 1);
      if Pos(tempAddress[i], IETF_QUOTABLE) > 0 then
      begin
        localPart := localPart + '\'; {Do not Localize}
      end;
      localPart := localPart + tempAddress[i];
      System.Delete(tempAddress, 1, i);
      i := FindFirstNotOf(IETF_ATEXT, tempAddress);
    end;
    Result := '<' + localPart + tempAddress + '"' + domainPart + '>'; {Do not Localize}
  end;
end;

function TWideIdEMailAddressItem.GetDomain: Widestring;
var i: Integer;
begin
  Result := '';
  for i := Length(FAddress) downto 1 do
  begin
    if FAddress[i] = '@' then {Do not Localize}
    begin
      Result := Copy(FAddress, i + 1, MaxInt);
      Break;
    end;
  end;
end;

procedure TWideIdEMailAddressItem.SetDomain(const ADomain: Widestring);
var
  Result: Widestring;
begin
  Result := FAddress;
  Delete(Result, Pos('@', Result) - 1, Length(Result));
  Result := Result + '@' + ADomain;
  FAddress := Result;
end;

function TWideIdEMailAddressItem.GetUsername: Widestring;
var i: Integer;
begin
  Result := '';
  for i := Length(FAddress) downto 1 do
  begin
    if FAddress[i] = '@' then {Do not Localize}
    begin
      Result := Copy(FAddress, 1, i - 1);
      Break;
    end;
  end;
end;

procedure TWideIdEMailAddressItem.SetUsername(const AUsername: Widestring);
var
  Result: Widestring;
begin
  Result := FAddress;
  Delete(Result, 1, Pos('@', Result) + 1);
  Result := AUsername + '@' + Result;
  FAddress := Result;
end;

function TWideIdEMailAddressItem.GetText: Widestring;
var
  i: Integer;
  tempName, resName: Widestring;
begin
  if (FName <> '') and (UpperCase(FAddress) <> FName) then
  begin
    i := FindFirstNotOf(IETF_ATEXT_SPACE, FName);
    if i > 0 then
    begin
      // Need to quote the FName.
      resName := '"' + Copy(FName, 1, i - 1); {Do not Localize}
      if Pos(FName[i], IETF_QUOTABLE) > 0 then
      begin
        resName := resName + '\'; {Do not Localize}
      end;
      resName := resName + FName[i];

      tempName := Copy(FName, i + 1, MaxInt);
      while tempName <> '' do
      begin
        i := FindFirstNotOf(IETF_ATEXT_SPACE, tempName);
        if i = 0 then
        begin
          Result := resName + tempName + '" ' + ConvertAddress; {Do not Localize}
          Exit;
        end;
        resName := resName + Copy(tempName, 1, i - 1);
        if Pos(tempName[i], IETF_QUOTABLE) > 0 then
        begin
          resName := resName + '\'; {Do not Localize}
        end;
        resName := resName + tempName[i];
        System.Delete(tempName, 1, i);
      end;
      Result := resName + '" ' + ConvertAddress; {Do not Localize}
    end else
    begin
      Result := FName + ' ' + ConvertAddress; {Do not Localize}
    end;
  end //  if
  else
  begin
    Result := ConvertAddress;
  end; // else .. if
end;

procedure TWideIdEMailAddressItem.SetText(AText: Widestring);
var
  nFirst,
    nBracketCount: Integer;
  bInAddress,
    bAddressInLT,
    bAfterAt,
    bInQuote: Boolean;
begin
  FAddress := ''; {Do not Localize}
  FName := ''; {Do not Localize}

  AText := Trim(AText);
  if AText = '' then
    Exit;

  // Find the first known character type.
  nFirst := FindFirstOf('("< @' + WideTab, AText); {Do not Localize}
  if nFirst <> 0 then
  begin
    nBracketCount := 0;
    bInAddress := False;
    bAddressInLT := False;
    bInQuote := False;
    bAfterAt := False;
    repeat
      case AnsiChar(AText[nFirst]) of
        ' ', #9: {Do not Localize}
          begin
            if nFirst = 1 then
            begin
              Delete(AText, 1, 1);
            end else
            begin
            // Only valid if in a name not contained in quotes - keep the space.
              if bAfterAt then begin
                FAddress := FAddress + Trim(Copy(AText, 1, nFirst - 1));
              end else begin
                FName := FName + Copy(AText, 1, nFirst);
              end;
              System.Delete(AText, 1, nFirst);
            end;
          end;
        '(': {Do not Localize}
          begin
            Inc(nBracketCount);
            if (nFirst > 1) then
            begin
            // There's at least one character to the name    {Do not Localize}
              if bInAddress then
              begin
                FAddress := FAddress + Trim(Copy(AText, 1, nFirst - 1));
              end else
              begin
                if nBracketCount = 1 then
                begin
                  FName := FName + Copy(AText, 1, nFirst - 1);
                end;
              end;
              System.Delete(AText, 1, nFirst);
            end else
            begin
              Delete(AText, 1, 1);
            end;
          end;
        ')': {Do not Localize}
          begin
            Dec(nBracketCount);
            System.Delete(AText, 1, nFirst);
          end;
        '"': {Do not Localize}
          begin
            if bInQuote then
            begin
              if bAddressInLT then
              begin
                FAddress := FAddress + Trim(Copy(AText, 1, nFirst - 1));
              end else
              begin
                FName := FName + Trim(Copy(AText, 1, nFirst - 1));
              end;
              System.Delete(AText, 1, nFirst);
              bInQuote := False;
            end else
            begin
              bInQuote := True;
              Delete(AText, 1, 1);
            end;
          end;
        '<': {Do not Localize}
          begin
            if nFirst > 1 then
            begin
              FName := FName + Copy(AText, 1, nFirst - 1);
            end;
            FName := TrimAllOf(' ' + #9, Trim(FName)); {Do not Localize}
            bAddressInLT := True;
            bInAddress := True;
            Delete(AText, 1, nFirst);
          end;
        '>': {Do not Localize}
          begin
          // Only searched for if the address starts with '<'    {Do not Localize}
            bInAddress := False;
            bAfterAt := False;
            FAddress := FAddress +
              TrimAllOf(' ' + #9, Trim(Copy(AText, 1, nFirst - 1))); {Do not Localize}
            System.Delete(AText, 1, nFirst);
          end;
        '@': {Do not Localize}
          begin
            bAfterAt := True;
            if bInAddress then
            begin
              FAddress := FAddress + Copy(AText, 1, nFirst);
              System.Delete(AText, 1, nFirst);
            end else
            begin
              if bAddressInLT then
              begin
              // Strange use. For now raise an exception until a real-world
              // example can be found.
              // Basically, it's formatted as follows:    {Do not Localize}
              //    <someguy@domain.example> some-text @ some-text
              // or:
              //    some-text <someguy@domain.example> some-text @ some-text
              // where some text may be blank.
              //CC: Note you used to arrive here if the From header in an email
              //included more than one address (which was subsequently changed)
              //because our code did not parse the From header for multiple
              //addresses.  That may have been the reason for this code.
//              raise EIdEmailParseError.Create(RSEMailSymbolOutsideAddress);
                fName := fName + AText;
                Exit;
              end else
              begin
              // If at this point, we're either supporting an e-mail address    {Do not Localize}
              // on it's own, or the old-style valid format:    {Do not Localize}
              //    "Name" name@domain.example
                bInAddress := True;
                FAddress := FAddress + Copy(AText, 1, nFirst);
                System.Delete(AText, 1, nFirst);
              end;
            end;
          end;
        '.': {Do not Localize}
          begin
          // Must now be a part of the domain part of the address.
            if bAddressInLT then
            begin
            // Whitespace is possible around the parts of the domain.
              FAddress := FAddress +
                TrimAllOf(' ' + #9, Trim(Copy(AText, 1, nFirst - 1))) + '.'; {Do not Localize}
              AText := TrimLeft(Copy(AText, nFirst + 1, MaxInt));
            end else
            begin
            // No whitespace is allowed if no wrapping <> characters.
              FAddress := FAddress + Copy(AText, 1, nFirst);
              System.Delete(AText, 1, nFirst);
            end;
          end;
        '\': {Do not Localize}
          begin
          // This will only be discovered in a bracketted or quoted section.
          // It's an escape character indicating the next cahracter is    {Do not Localize}
          // a literal.
            if bInQuote then
            begin
            // Need to retain the second character
              if bInAddress then
              begin
                FAddress := FAddress + Copy(AText, 1, nFirst - 1);
                FAddress := FAddress + AText[nFirst + 1];
              end else
              begin
                FName := FName + Copy(AText, 1, nFirst - 1);
                FName := FName + AText[nFirst + 1];
              end;
            end;
            System.Delete(AText, 1, nFirst + 1);
          end;
      end;


      // Check for bracketted sections first: ("<>" <> "" <"">) - all is ignored
      if nBracketCount > 0 then
      begin
        // Inside a bracket, only three charatcers are special.
        // '(' Opens a nested bracket: (One (Two (Three )))    {Do not Localize}
        // ')' Closes a bracket    {Do not Localize}
        // '/' Escape character: (One /) /( // (Two /) ))    {Do not Localize}
        nFirst := FindFirstOf('()\', AText); {Do not Localize}

      // Check if in quote before address: <"My Name"@domain.example> is valid
      end else if bInQuote then
      begin
      // Inside quotes, only the end quote and escape character are special.
        nFirst := FindFirstOf('"\', AText); {Do not Localize}

      // Check if after the @ of the address: domain.example>
      end else if bAfterAt then
      begin
        if bAddressInLT then
        begin
          // If the address is enclosed, then only the '(', '.' & '>' need be    {Do not Localize}
          // looked for, trimming all content when found: domain  .  example >
          nFirst := FindFirstOf('.>(', AText); {Do not Localize}
        end else
        begin
          nFirst := FindFirstOf('.( ', AText); {Do not Localize}
        end;

      // Check if in address: <name@domain.example>
      end else if bInAddress then
      begin
        nFirst := FindFirstOf('"(@>', AText); {Do not Localize}

      // Not in anything - check for opening charactere
      end else
      begin
        // Outside brackets
        nFirst := FindFirstOf('("< @' + WideTab, AText); {Do not Localize}
      end;
    until nFirst = 0;
    if bInAddress and not bAddressInLT then
    begin
      FAddress := FAddress + TrimAllOf(' ' + #9, Trim(AText)); {Do not Localize}
    end;
  end else
  begin
    // No special characters, so assume a simple address
    FAddress := AText;
  end;
end;



{ TWideIdEMailAddressList }

function TWideIdEMailAddressList.Add: TWideIdEMailAddressItem;
begin
  Result := TWideIdEMailAddressItem(inherited Add);
end;

constructor TWideIdEMailAddressList.Create(AOwner: TPersistent);
begin
  inherited Create(AOwner, TWideIdEMailAddressItem);
end;

procedure TWideIdEMailAddressList.FillTStrings(AStrings: TTntStrings);
var
  idx: Integer;
begin
  for idx := 0 to Count - 1 do
  begin
    AStrings.Add(GetItem(idx).Text);
  end;
end;

function TWideIdEMailAddressList.GetItem(Index: Integer): TWideIdEMailAddressItem;
begin
  Result := TWideIdEMailAddressItem(inherited Items[Index]);
end;

function TWideIdEMailAddressList.GetEMailAddresses: Widestring;
var
  idx: Integer;
begin
  Result := ''; {Do not Localize}
  for idx := 0 to Count - 1 do
  begin
    if Result = '' then
      Result := GetItem(idx).Text
    else
      Result := Result + ', ' + GetItem(idx).Text; {Do not Localize}
  end;
end;

procedure TWideIdEMailAddressList.SetItem(Index: Integer;
  const Value: TWideIdEMailAddressItem);
begin
  inherited SetItem(Index, Value);
end;

procedure TWideIdEMailAddressList.SetEMailAddresses(AList: Widestring);
var
  EMail: TWideIdEMailAddressItem;
  iStart: Integer;
  sTemp: Widestring;
  nInBracket: Integer;
  bInQuote: Boolean;
begin
  Clear;

  if (Trim(AList) = '') then Exit; {Do not Localize}

  iStart := FindFirstOf(':;(", ' + WideTab, AList); {Do not Localize}
  if iStart = 0 then begin
    EMail := Add;
    EMail.Text := TrimLeft(AList);
  end else begin
    sTemp := ''; {Do not Localize}
    nInBracket := 0;
    bInQuote := False;
    repeat
      case AList[iStart] of
        ' ', #9: begin {Do not Localize}
            if iStart = 1 then begin
              sTemp := sTemp + AList[iStart];
              System.Delete(AList, 1, 1);
            end else begin
              sTemp := sTemp + Copy(AList, 1, iStart);
              System.Delete(AList, 1, iStart);
            end;
          end;
        ':': {Do not Localize}
          begin
          // The start of a group - ignore the lot.
            System.Delete(AList, 1, iStart);
            sTemp := ''; {Do not Localize}
          end;
        ';': {Do not Localize}
          begin
          // End of a group.  If we have something (groups can be empty),
          // then process it.
            sTemp := sTemp + Copy(AList, 1, iStart - 1);
            if Trim(sTemp) <> '' then begin
              EMail := Add;
              EMail.Text := TrimLeft(sTemp);
              sTemp := ''; {Do not Localize}
            end;
          // Now simply remove the end of the group.
            System.Delete(AList, 1, iStart);
          end;
        '(': begin {Do not Localize}
            Inc(nInBracket);
            sTemp := sTemp + Copy(AList, 1, iStart);
            System.Delete(AList, 1, iStart);
          end;
        ')': begin {Do not Localize}
            Dec(nInBracket);
            sTemp := sTemp + Copy(AList, 1, iStart);
            System.Delete(AList, 1, iStart);
          end;
        '"': begin {Do not Localize}
            sTemp := sTemp + Copy(AList, 1, iStart);
            System.Delete(AList, 1, iStart);
            bInQuote := not bInQuote;
          end;
        ',': begin {Do not Localize}
            sTemp := sTemp + Copy(AList, 1, iStart - 1);
            EMail := Add;
            EMail.Text := sTemp;
          // added - Allen .. saves blank entries being added
            if (Trim(Email.Text) = '') or (Trim(Email.Text) = '<>') then {Do not Localize}
            begin
              FreeAndNil(Email);
            end;
            sTemp := ''; {Do not Localize}
            System.Delete(AList, 1, iStart);
          end;
        '\': begin {Do not Localize}
          // Escape character - simply copy this char and the next to the buffer.
            sTemp := sTemp + Copy(AList, 1, iStart + 1);
            System.Delete(AList, 1, iStart + 1);
          end;
      end;

      if nInBracket > 0 then begin
        iStart := FindFirstOf('(\)', AList); {Do not Localize}
      end else if bInQuote then begin
        iStart := FindFirstOf('"\', AList); {Do not Localize}
      end else begin
        iStart := FindFirstOf(':;(", ' + WideTab, AList); {Do not Localize}
      end;
    until iStart = 0;

    // Clean up the content in sTemp
    if (Trim(sTemp) <> '') or (Trim(AList) <> '') then begin
      sTemp := sTemp + AList;
      EMail := Add;
      EMail.Text := TrimLeft(sTemp);
      // added - Allen .. saves blank entries being added
      if (Trim(Email.Text) = '') or (Trim(Email.Text) = '<>') then {Do not Localize}
      begin
        FreeAndNil(Email);
      end;
    end;
  end;
end;

procedure TWideIdEMailAddressList.SortByDomain;
var
  i, j: Integer;
  LTemp: Widestring;
begin
  for i := Count - 1 downto 0 do
  begin
    for j := 0 to Count - 2 do
    begin
      if WideCompareStr(Items[J].Domain, Items[J + 1].Domain) > 0 then begin
        LTemp := Items[j].Text;

        Items[j].Text := Items[j + 1].Text;
        Items[j + 1].Text := LTemp;
      end;
    end;
  end;
end;

procedure TWideIdEMailAddressList.GetDomains(AStrings: TTntStrings);
var
  i: Integer;
  LCurDom: Widestring;
begin
  if Assigned(AStrings) then
  begin
    AStrings.Clear;
    for i := 0 to Count - 1 do
    begin
      LCurDom := Lowercase(Items[i].Domain);
      if AStrings.IndexOf(LCurDom) = -1 then
      begin
        AStrings.Add(LCurDom);
      end;
    end;
  end;
end;

procedure TWideIdEMailAddressList.AddressesByDomain(AList: TWideIdEMailAddressList;
  const ADomain: Widestring);
var
  i: Integer;
  LDomain: Widestring;
  LCurDom: Widestring;
  LEnt: TWideIdEMailAddressItem;
begin
  LDomain := LowerCase(ADomain);
  AList.Clear;
  for i := 0 to Count - 1 do
  begin
    LCurDom := LowerCase(Items[i].Domain);
    if LCurDom = LDomain then
    begin
      LEnt := AList.Add;
      LEnt.Text := Items[i].Text;
    end;
  end;
end;

end.

