How can a Delphi Program send an Email with Attachments via the DEFAULT E-mail Client?
Asked Answered
H

5

14

Within my program, I am composing an email to send using the default e-mail client software installed on a user's machine.

I have composed the mailto address, the subject, the multilined body, and I have several attachments to be included.

I almost got this working using mailto and ShellExecute as follows:

  Message := 'mailto:[email protected]'
    + '?subject=This is the subjectBehold Error Report'
    + '&body=This is line 1' + '%0D%0A'
    + 'This is line 2' + '%0D%0A'
    + 'This is line 3'
    + '&Attach=c:\file1.txt';
  RetVal := ShellExecute(Handle, 'open', PChar(Message), nil, nil, SW_SHOWNORMAL);
  if RetVal <= 32 then
    MessageDlg('Cannot find program to send e-mail.', mtWarning, [mbOK], 0);

Using Delphi 2009 on a Windows Vista machine, this will open a Microsoft Mail "Create Mail" window, with the To, Subject and Body filled correctly. However the file does not get attached.

As I researched this, I noticed some commentary stating that this technique does not work with all mail clients. However, most of the commentary is fairly old, as I realize this is a very old technique.

Then I found that Zarko Gajic said that "this approach is ok, but you are unable to send attachments in this way".

I have seen theres also the Windows Simple Mail API (MAPI), but Zarko says that only works if the end-user has MAPI-compliant email software. There are well documented techniques on using MAPI with Delphi (e.g. Sending e-mail using mapi), but they all have the disclaimer that MAPI is not always installed with Windows.

Besides, I really want the message to come up first in the user's default email program, so they will have it as part of their email records and they can edit it and decide if and when they want to send it. I'm not sure how MAPI works and if it will do that.

So my requirements are:

  1. To bring the email up in the user's mail program.

  2. To allow one or more attachments.

  3. To work with (hopefully) all email clients on any Windows machine from XP up (i.e. XP, Vista or 7).

Is there such an animal? Or maybe does someone know how to get attachments to work with the mailto/ShellExecute technique?

What do most people do?


Edit:

There have been a few answers with MAPI solutions and even an Indy solution.

The problem I have with them is that they don't necessarily use the default mail client. On my Vista machine, for example, I have set up Windows Mail as my default client. When I do a MAPI send, it does not bring up Windows Mail, but it brings up and sets up the email in Outlook instead. I don't want that.

Two of my users of my program complained:

Your debug routine fails to send the file, as it tries to start windows mail for some reason known to it's self rather than using the default mail client (in my case thunderbird)

I tried to fill up the exception report but gave up when it asked for this server, that server! I then got really annoyed because it launched Outlook - I never, ever use it or want to use it.

I don't need code for MAPI or Indy. They are readily available. But if you suggest MAPI or Indy, what I really need is a way to find the default client and ensure that it is the one that is passed the email to be sent.

Also, I need to know if MAPI is now universal. 5 years ago, it wasn't guaranteed to work on all machines because it wasn't installed as part of the operating system. Is that still true, or does MAPI now come with Windows XP, Vista and 7 by default?

Same questions go for Indy or any other suggested solutions. Can it work with the default client and will it work on almost all Windows XP and later machines?

The reason why the "mailto" solution is so nice, is that all machines have to support it for the purpose of handling the HTML mailto statement found on webpages. Now if only I could use it to add attachments ...


Likely solution found: mjustin pointed out an alternative that makes use of the Operating System's sendto command. That most likely is the way to go.

The mailto was not limited to 256 characters like the HTML mailto is, but I was devastated to find out it ended up being limited to 2048 characters. Fortunately a few hours later, mjustin gave his answer.

If implementation of that goes okay, his answer will have done it for me. If not, I'll add my comments here.


No. As it turns out, the sendto solution will not always open the default email program. On my machine, it opens Outlook when my default mailer is Windows Mail. Too bad. I've had to go back to the mailto method, despite the 2048 character limit.

I did, however, find in the article: SendTo mail recipient that:

At this point, you could replace ::ShellExecute with a well thought ::WinExec call, using the actual mailto command line declared in the registry and target the current e-mail client (for instance, "%ProgramFiles%\Outlook Express\msimn.exe" /mailurl:%1). But then the limitation is 32 KB. As a conclusion, there is no way to send e-mails larger than 32KB using the mailto protocol.

but then I'd have to determine who the mail client is in each case. I expect that would lead to further complications.

The one other thing I found out is that mailto allows setting of "to", "cc", "bcc", "subject" and "body" but no attachments. Whereas sendto ONLY allows attachments and then sets up a default email with a default message and no way for you to set the various fields and body.

Hedgepeth answered 26/12, 2009 at 6:47 Comment(16)
see delphi.about.com/od/indy/a/email-send-indy.htmBiotype
using the default mail clientTropism
That was the link I had in my question, but I didn't see anything there that indicated that Indy uses the default mail client. How can that be if it needs to be given the SMTP mail server? Please explain.Hedgepeth
You do need MAPI code because that's the way to send arbitrary e-mail using the default client. If the default client doesn't support MAPI, then you're out of luck. Indy cannot send mail using any other client; Indy is the e-mail client in that case. If MAPI isn't finding the right program, then that's the problem you should investigate further.Traumatize
Rob: If MAPI were built into the Operating System, then I'd feel better about it. With XP, people had to install Outlook (i.e. Microsoft Office) before they'd get it. You are right this is a problem. But I've got everything working fine with mailto/ShellExecute except that it doesn't support attachments ... but all this thinking is leading me to an idea :-) I'll post another question.Hedgepeth
As it turns out, I don't have to create another question, since there already is one: #198988 My program might work just as well by presenting the files to attach and allowing the user to drag and drop them to their email client - not quite as slick as adding them directly, but it will work.Hedgepeth
With windows XP simple mapi is "part" of the system because it comes with Outlook Express. Not true for Windows Vista or 7. But if they don't have a mail client installed, how will you send mail with it anyway?Tropism
Runner: They may install Thunderbird, or simply use a web-based mail service. For those, the mailto method will likely work.Hedgepeth
Indy uses SMTP directly. It cannot send mail through default mail client.Tropism
Rob, even an easier way: I can copy all the file references to the clipboard and then all they have to do is do the paste into their client, a 'la: https://mcmap.net/q/461672/-copy-files-to-clipboard-in-cHedgepeth
And if you ask me go the MAPI route. The "hack" with the attachment is not worth the trouble. What if you body will be longer than 255 characters? ShellExecute does not handle that.Tropism
If they install Thunderbird MAPI is installed with it. As I said all mail clients that I know use MAPI also. Outlook and Thunderbird have a prevalent market share. Will mailto work if they use a web client?Tropism
Runner: ShellExecute works fine with more than 255 characters. You are thinking of the HTML mailto that is limited to that by the maximum length of a URL.Hedgepeth
Ikessler, any time mailto works, whether in a URL or in a shell command, you have been lucky. There is no such URL standard, so programs are liable to interpret it differently without warning. Some support all kinds of options — subject, body, bcc, etc. — whereas others support a single e-mail address. I see no reason for shell mailto to work on Web clients; there is no mail client registered with the OS. URL mailto only works with Web clients like Gmail if the Google toolbar is installed to catch such URLs in the browser. Drag-and-drop or clipboard files won't work with Web clients anyway.Traumatize
Rob: I found the mailto working okay. I tried it on XP and Vista and Windows 7, but of course you have to have have a default email program installed which Vista and 7 don't. The sendto solution will probably have the same characteristics but at least some advantages that make it worthwhile switching to. Not sure how it'll work with Web clients, but at least it won't require drag and drop.Hedgepeth
@lkessler-wow. You really dig for it! Cool post. I fight with EurekaLog to convince it to send proper emails (I really started to get annoyed by EurekaLog 7). I hope all this info will help me.Tower
H
4

It appears that mailto in a ShellExecute is not capable of sending attachments.

MAPI and Indy have the unfortunate characteristic of not necessarily selecting the user's email client.

So the other possibility is to continue using ShellExecute, but find another way to get the attachments into the email client.

What I decided to do was on my Dialog that creates the email, I now have a FileListBox listing the files the user may want to attach to the email. When the email pops up, they can simply drag and drop them to the email.

In my case, this is actually a good solution, since this allows the users to select the files they want to include. The other method (automatically attaching them) will require they delete the ones they don't want included. (i.e. Having the "Add Google toolbar" option already checked for you is NOT good)

For the time being this solution will work.

Thank you to all those who contributed answers and helped me see my way through this (all +1).

Hedgepeth answered 27/12, 2009 at 1:40 Comment(0)
T
5

Do not complicate, just use the JCL MAPI code. It is in the unit JclMapi.pas. I think they also have the example for it. The code is very powerfull and you can do anything that MAPI allows you.

With ShellExecute you cannot send the attachment and you are also limited to 255 characters for the mail body.

As long as MAPI goes, with old windows it is always installed (2000, XP). It comes together with Outlook Express and Outlook Express is almost always installed. With newer windows (Vista, 7) there is no Outlook Express and so no MAPI. But MAPI is automatically installed if you install MS Outlook or Mozzila Thunderbird. So you are pretty safe. This is basic MAPI and not extended MAPI. But it covers all you need.

You can also check in your code (JCL) if MAPI is installed and act acordingly. I have done a similar thing not so long ago and it works ok. I have not found a popular windows mail client that does not support simple MAPI. This is a simple wrapper around JCL code and the sample usage bellow:

unit MAPI.SendMail;

interface

uses
  SysUtils, Classes, JclMapi;

type
  TPrerequisites = class
  public
    function IsMapiAvailable: Boolean;
    function IsClientAvailable: Boolean;
  end;

  TMAPISendMail = class
  private
    FAJclEmail: TJclEmail;
    FShowDialog: Boolean;
    FResolveNames: Boolean;
    FPrerequisites: TPrerequisites;
    // proxy property getters
    function GetMailBody: string;
    function GetHTMLBody: Boolean;
    function GetMailSubject: string;
    // proxy property setters
    procedure SetMailBody(const Value: string);
    procedure SetHTMLBody(const Value: Boolean);
    procedure SetMailSubject(const Value: string);
  protected
    function DoSendMail: Boolean; virtual;
  public
    constructor Create;
    destructor Destroy; override;
    // properties of the wrapper class
    property MailBody: string read GetMailBody write SetMailBody;
    property HTMLBody: Boolean read GetHTMLBody write SetHTMLBody;
    property ShowDialog: Boolean read FShowDialog write FShowDialog;
    property MailSubject: string read GetMailSubject write SetMailSubject;
    property ResolveNames: Boolean read FResolveNames write FResolveNames;
    property Prerequisites: TPrerequisites read FPrerequisites;
    // procedure and functions of the wrapper class
    procedure AddRecipient(const Address: string; const Name: string = '');
    procedure AddAttachment(const FileName: string);
    function SendMail: Boolean;
  end;

implementation

{ TMAPISendMail }

constructor TMAPISendMail.Create;
begin
  FPrerequisites := TPrerequisites.Create;
  FAJclEmail := TJclEmail.Create;
  FShowDialog := True;
end;

destructor TMAPISendMail.Destroy;
begin
  FreeAndNil(FAJclEmail);
  FreeAndNil(FPrerequisites);

  inherited;
end;

function TMAPISendMail.DoSendMail: Boolean;
begin
  Result := FAJclEmail.Send(FShowDialog);
end;

function TMAPISendMail.SendMail: Boolean;
begin
  Result := DoSendMail;
end;

function TMAPISendMail.GetMailBody: string;
begin
  Result := FAJclEmail.Body;
end;

procedure TMAPISendMail.SetMailBody(const Value: string);
begin
  FAJclEmail.Body := Value;
end;

procedure TMAPISendMail.AddAttachment(const FileName: string);
begin
  FAJclEmail.Attachments.Add(FileName);
end;

procedure TMAPISendMail.AddRecipient(const Address, Name: string);
var
  LocalName: string;
  LocalAddress: string;
begin
  LocalAddress := Address;
  LocalName := Name;

  if FResolveNames then
    if not FAJclEmail.ResolveName(LocalName, LocalAddress) then
      raise Exception.Create('Could not resolve Recipient name and address!');

  FAJclEmail.Recipients.Add(LocalAddress, LocalName);
end;

function TMAPISendMail.GetMailSubject: string;
begin
  Result := FAJclEmail.Subject;
end;

procedure TMAPISendMail.SetMailSubject(const Value: string);
begin
  FAJclEmail.Subject := Value;
end;

function TMAPISendMail.GetHTMLBody: Boolean;
begin
  Result := FAJclEmail.HtmlBody;
end;

procedure TMAPISendMail.SetHTMLBody(const Value: Boolean);
begin
  FAJclEmail.HtmlBody := Value;
end;

{ TPrerequisites }

function TPrerequisites.IsClientAvailable: Boolean;
var
  SimpleMAPI: TJclSimpleMapi;
begin
  SimpleMAPI := TJclSimpleMapi.Create;
  try
    Result := SimpleMAPI.AnyClientInstalled;
  finally
    SimpleMAPI.Free;
  end;
end;

function TPrerequisites.IsMapiAvailable: Boolean;
var
  SimpleMAPI: TJclSimpleMapi;
begin
  SimpleMAPI := TJclSimpleMapi.Create;
  try
    Result := SimpleMAPI.SimpleMapiInstalled;
  finally
    SimpleMAPI.Free;
  end;
end;

end.

Sample usage:

unit f_Main;

interface

uses
  Windows, SysUtils, Classes, Controls, Forms, Graphics, StdCtrls, XPMan,

  // project units
  JclMapi, MAPI.SendMail, Dialogs;

type
  TfMain = class(TForm)
    XPManifest: TXPManifest;
    gbMailProperties: TGroupBox;
    eMailSubject: TEdit;
    stMailSubject: TStaticText;
    stMailBody: TStaticText;
    mmMailBody: TMemo;
    cbHTMLBody: TCheckBox;
    gbAttachments: TGroupBox;
    gbRecipients: TGroupBox;
    btnSendMail: TButton;
    lbRecipients: TListBox;
    eRecipAddress: TEdit;
    StaticText1: TStaticText;
    eRecipName: TEdit;
    btnAddRecipient: TButton;
    stRecipName: TStaticText;
    OpenDialog: TOpenDialog;
    lbAttachments: TListBox;
    btnAddAttachment: TButton;
    stMAPILabel: TStaticText;
    stClientLabel: TStaticText;
    stMAPIValue: TStaticText;
    stClientValue: TStaticText;
    procedure btnSendMailClick(Sender: TObject);
    procedure btnAddRecipientClick(Sender: TObject);
    procedure btnAddAttachmentClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  fMain: TfMain;

implementation

{$R *.dfm}

procedure TfMain.btnSendMailClick(Sender: TObject);
var
  I: Integer;
  Name: string;
  Address: string;
  ItemStr: string;
  Pos1, Pos2: Integer;
  MAPISendMail: TMAPISendMail;
begin
  MAPISendMail := TMAPISendMail.Create;
  try
    for I := 0 to lbRecipients.Items.Count - 1 do
    begin
      ItemStr := lbRecipients.Items[I];
      Pos1 := Pos('[', ItemStr);
      Pos2 := Pos(']', ItemStr);

      Name := Trim(Copy(ItemStr, Pos1 + 1, Pos2 - Pos1 - 1));
      Address := Trim(Copy(ItemStr, 1, Pos1 - 1));
      MAPISendMail.AddRecipient(Address, Name);
    end;

    for I := 0 to lbAttachments.Items.Count - 1 do
      MAPISendMail.AddAttachment(lbAttachments.Items[I]);

    MAPISendMail.MailSubject := eMailSubject.Text;
    MAPISendMail.HTMLBody := cbHTMLBody.Checked;
    MAPISendMail.MailBody := mmMailBody.Text;
    MAPISendMail.SendMail;
  finally
    MAPISendMail.Free;
  end;
end;

procedure TfMain.btnAddRecipientClick(Sender: TObject);
begin
  lbRecipients.Items.Add(Format('%s [%s]', [eRecipAddress.Text,
                                            eRecipName.Text]));
end;

procedure TfMain.btnAddAttachmentClick(Sender: TObject);
begin
  if OpenDialog.Execute then
    lbAttachments.Items.Add(OpenDialog.FileName);
end;

procedure TfMain.FormCreate(Sender: TObject);
var
  ValidHost: Boolean;
  MAPISendMail: TMAPISendMail;
begin
  MAPISendMail := TMAPISendMail.Create;
  try
    ValidHost := True;

    if MAPISendMail.Prerequisites.IsMapiAvailable then
    begin
      stMAPIValue.Caption := 'Available';
      stMAPIValue.Font.Color := clGreen;
    end
    else
    begin
      stMAPIValue.Caption := 'Unavailable';
      stMAPIValue.Font.Color := clRed;
      ValidHost := False;
    end;

    if MAPISendMail.Prerequisites.IsClientAvailable then
    begin
      stClientValue.Caption := 'Available';
      stClientValue.Font.Color := clGreen;
    end
    else
    begin
      stClientValue.Caption := 'Unavailable';
      stClientValue.Font.Color := clRed;
      ValidHost := False;
    end;

    btnSendMail.Enabled := ValidHost;
  finally
    MAPISendMail.Free;
  end;
end;

end.
Tropism answered 26/12, 2009 at 14:35 Comment(0)
G
4

I use two methods for sending a MAPI mail, depending on whether an attatchment is needed. For the simple case with no attachment I use the following:

function SendShellEmail( ARecipientEmail, ASubject, ABody : string ) : boolean;
// Send an email to this recipient with a subject and a body
var
  iResult : integer;
  S       : string;
begin

 If Trim(ARecipientEmail) = '' then
   ARecipientEmail := 'mail';
 S := 'mailto:' + ARecipientEmail;

 S := S + '?subject=' + ASubject;

 If Trim(ABody) <> '' then
  S := S + '&body=' + ABody;

 iResult := ShellExecute( Application.Handle,'open', PChar(S), nil, nil, SW_SHOWNORMAL);
 Result := iResult > 0;
end;

This uses a simple shell execute method, so you should not have any real problems other than the more recent alerts to get the User to confirm that they are ok with your program sending an email.

For attatchments I use the following code originally taken from the Delphi Magazine by Brian Long. It is also possible to send an email WITHOUT using the MAPI client but using a nominated SMTP server but I think you explicitly do not want this. I can provide code for this if you do.

uses
  SysUtils,
  Windows,
  Dialogs,
  Forms,
  MAPI;

procedure ArtMAPISendMail(
            const Subject, MessageText, MailFromName, MailFromAddress,
                  MailToName, MailToAddress: String;
            const AttachmentFileNames: array of String);
//Originally by Brian Long: The Delphi Magazine issue 60 - Delphi And Email
var
  MAPIError: DWord;
  MapiMessage: TMapiMessage;
  Originator, Recipient: TMapiRecipDesc;
  Files, FilesTmp: PMapiFileDesc;
  FilesCount: Integer;
begin
   FillChar(MapiMessage, Sizeof(TMapiMessage), 0);

   MapiMessage.lpszSubject := PAnsiChar(AnsiString(Subject));
   MapiMessage.lpszNoteText := PAnsiChar(AnsiString(MessageText));

   FillChar(Originator, Sizeof(TMapiRecipDesc), 0);

   Originator.lpszName := PAnsiChar(AnsiString(MailFromName));
   Originator.lpszAddress := PAnsiChar(AnsiString(MailFromAddress));
//   MapiMessage.lpOriginator := @Originator;
   MapiMessage.lpOriginator := nil;


   MapiMessage.nRecipCount := 1;
   FillChar(Recipient, Sizeof(TMapiRecipDesc), 0);
   Recipient.ulRecipClass := MAPI_TO;
   Recipient.lpszName := PAnsiChar(AnsiString(MailToName));
   Recipient.lpszAddress := PAnsiChar(AnsiString(MailToAddress));
   MapiMessage.lpRecips := @Recipient;

   MapiMessage.nFileCount := High(AttachmentFileNames) - Low(AttachmentFileNames) + 1;
   Files := AllocMem(SizeOf(TMapiFileDesc) * MapiMessage.nFileCount);
   MapiMessage.lpFiles := Files;
   FilesTmp := Files;
   for FilesCount := Low(AttachmentFileNames) to High(AttachmentFileNames) do
   begin
     FilesTmp.nPosition := $FFFFFFFF;
     FilesTmp.lpszPathName := PAnsiChar(AnsiString(AttachmentFileNames[FilesCount]));
     Inc(FilesTmp)
   end;

   try
     MAPIError := MapiSendMail(
       0,
       Application.MainForm.Handle,
       MapiMessage,
       MAPI_LOGON_UI {or MAPI_NEW_SESSION},
       0);
   finally
     FreeMem(Files)
   end;

   case MAPIError of
     MAPI_E_AMBIGUOUS_RECIPIENT:
      Showmessage('A recipient matched more than one of the recipient descriptor structures and MAPI_DIALOG was not set. No message was sent.');
     MAPI_E_ATTACHMENT_NOT_FOUND:
      Showmessage('The specified attachment was not found; no message was sent.');
     MAPI_E_ATTACHMENT_OPEN_FAILURE:
      Showmessage('The specified attachment could not be opened; no message was sent.');
     MAPI_E_BAD_RECIPTYPE:
      Showmessage('The type of a recipient was not MAPI_TO, MAPI_CC, or MAPI_BCC. No message was sent.');
     MAPI_E_FAILURE:
      Showmessage('One or more unspecified errors occurred; no message was sent.');
     MAPI_E_INSUFFICIENT_MEMORY:
      Showmessage('There was insufficient memory to proceed. No message was sent.');
     MAPI_E_LOGIN_FAILURE:
      Showmessage('There was no default logon, and the user failed to log on successfully when the logon dialog box was displayed. No message was sent.');
     MAPI_E_TEXT_TOO_LARGE:
      Showmessage('The text in the message was too large to sent; the message was not sent.');
     MAPI_E_TOO_MANY_FILES:
      Showmessage('There were too many file attachments; no message was sent.');
     MAPI_E_TOO_MANY_RECIPIENTS:
      Showmessage('There were too many recipients; no message was sent.');
     MAPI_E_UNKNOWN_RECIPIENT:
       Showmessage('A recipient did not appear in the address list; no message was sent.');
     MAPI_E_USER_ABORT:
       Showmessage('The user canceled the process; no message was sent.');
     SUCCESS_SUCCESS:
       Showmessage('MAPISendMail successfully sent the message.');
   else
     Showmessage('MAPISendMail failed with an unknown error code.');
   end;
end;
Goldschmidt answered 26/12, 2009 at 7:46 Comment(0)
H
4

It appears that mailto in a ShellExecute is not capable of sending attachments.

MAPI and Indy have the unfortunate characteristic of not necessarily selecting the user's email client.

So the other possibility is to continue using ShellExecute, but find another way to get the attachments into the email client.

What I decided to do was on my Dialog that creates the email, I now have a FileListBox listing the files the user may want to attach to the email. When the email pops up, they can simply drag and drop them to the email.

In my case, this is actually a good solution, since this allows the users to select the files they want to include. The other method (automatically attaching them) will require they delete the ones they don't want included. (i.e. Having the "Add Google toolbar" option already checked for you is NOT good)

For the time being this solution will work.

Thank you to all those who contributed answers and helped me see my way through this (all +1).

Hedgepeth answered 27/12, 2009 at 1:40 Comment(0)
H
2

This article shows how Delphi can simulate the "Send to..." shell context menu command and open the default mail client with attachments programatically.

This solution does not need MAPI and works with the default mail client, but is not complete, because the message recipients, body and subject will not be filled in automatically. (The message body could be copied using the clipboard).

Hecht answered 27/12, 2009 at 11:43 Comment(2)
Thank you. That's the alternative that I didn't know about that I was hoping someone would find for me.Hedgepeth
Nope. It worked for awhile loading into Windows Mail. Then inexplicably later today, it started loading into Outlook which is NOT my default mail program. Had to take away your "accepted answer". Sorry.Hedgepeth
T
1

Here is a summary about all those email settings and what they do:
http://thesunstroke.blogspot.de/2017/03/how-to-configure-eurekalog-to-send-bugs.html

enter image description here

So, stay away from Shell (mailto).
Mapi is also a bad idea since it only works with MS email clients.
I set by default Simple MAPI but I rarely receive emails sent by this channel. Most emails are received via SMTP Server.

BIG WARNING!!!!!!!!!
I have seen that the number of false positive alarms from antivirus scanners is much higher when you activate EurekaLog. So, only use EurekaLog when absolutely necessary.
Also, Eureka itself is peppered with bugs (just look at the release history and see that for each new feature (or even change) they release, they later fix a few bugs! So, if you are tracking for bugs, notice that EurekaLog itself may introduce few in your EXE!

Tower answered 19/5, 2017 at 11:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.