|
|
|
|
|
|
|
|
|
|
|
T U T O R I A L F O R M I M E + +
|
|
|
|
|
|
|
|
1. Introduction
|
|
|
|
|
|
|
|
Welcome to MIME++, a C++ class library for creating, parsing, and modifying
|
|
|
|
messages in MIME format. MIME++ has been designed specifically with the
|
|
|
|
following objectives in mind:
|
|
|
|
|
|
|
|
* Create classes that directly correspond to the elements described in
|
|
|
|
RFC-822, RFC-2045, and other MIME-related documents.
|
|
|
|
|
|
|
|
* Create a library that is easy to use.
|
|
|
|
|
|
|
|
* Create a library that is extensible.
|
|
|
|
|
|
|
|
MIME++ classes directly model the elements of the BNF grammar specified in
|
|
|
|
RFC-822, RFC-2045, and RFC-2046. For this reason, I recommend that you
|
|
|
|
understand these RFCs and keep a copy of them handy as you learn MIME++.
|
|
|
|
If you know C++ well, and if you are familiar with the RFCs, you should tqfind
|
|
|
|
MIME++ easy to learn and use. If you are new to C++ and object-oriented
|
|
|
|
programming, you will find in MIME++ some very good object-oriented
|
|
|
|
techinques, and hopefully you will learn a lot.
|
|
|
|
|
|
|
|
Before looking at the MIME++ classes, it is important to understand how
|
|
|
|
MIME++ represents a message. There are two representations of a message.
|
|
|
|
The first is a string representation, in which a message is considered
|
|
|
|
simply a sequence of characters. The second is a 'broken-down' -- that is,
|
|
|
|
parsed -- representation, in which the message is represented as a tree of
|
|
|
|
components.
|
|
|
|
|
|
|
|
The tree will be explained later, but for now, let's consider the
|
|
|
|
relationship between the string representation and the broken-down
|
|
|
|
representation. When you create a new message, the string representation
|
|
|
|
is initially empty. After you set the contents of the broken-down
|
|
|
|
representation, such as the header fields and the message body, you then
|
|
|
|
assemble the message from its broken-down representation into its string
|
|
|
|
representation. The assembling is done through a call to the Assemble()
|
|
|
|
member function of the DwMessage class. Conversely, when you receive a
|
|
|
|
message, it is received in its string representation, and you parse the
|
|
|
|
message to create its broken-down representation. The parsing is done
|
|
|
|
through a call to the Parse() member function of the DwMessage class.
|
|
|
|
From the broken-down representation, you can access the header fields, the
|
|
|
|
body, and so on. If you want to modify a received message, you can change
|
|
|
|
the contents of the broken-down representation, then assemble the message
|
|
|
|
to create the modified string representation. Because of the way MIME++
|
|
|
|
implements the broken-down representation, only those specific components
|
|
|
|
that were modified in the broken-down representation will be modified in
|
|
|
|
the new string representation.
|
|
|
|
|
|
|
|
The broken-down representation takes the form of a tree. The idea for the
|
|
|
|
tree comes from the idea that a message can be broken down into various
|
|
|
|
components, and that the components form a hierarchy. At the highest
|
|
|
|
level, we have the complete message. We can break the message down into a
|
|
|
|
header and a body to arrive at the second-highest level. We can break the
|
|
|
|
header down into a collection of header fields. We can break each header
|
|
|
|
field down into a field-name and a field-body. If the header field is a
|
|
|
|
structured field, we can further break down its field-body into components
|
|
|
|
specific to that field-body, such as a local-part and domain for a
|
|
|
|
mailbox. Now, we can think of each component of the message as a node in
|
|
|
|
the tree. The top, or root, node is the message itself. Below that, the
|
|
|
|
message node contains child nodes for the header and body; the header node
|
|
|
|
contains a child node for each header field; and so on. Each node
|
|
|
|
contains a substring of the entire message, and a node's string is the
|
|
|
|
concatenation of all of its child nodes' strings.
|
|
|
|
|
|
|
|
In the MIME++ implementation, the abstract base class DwMessageComponent
|
|
|
|
encapsulates all the common attributes and behavior of the tree's nodes.
|
|
|
|
The most important member functions of DwMessageComponent are Parse() and
|
|
|
|
Assemble(), which are declared as pure virtual functions. Normally, you
|
|
|
|
would use these member functions only as operations on objects of the
|
|
|
|
class DwMessage, a subclass of DwMessageComponent. Parse() builds the
|
|
|
|
entire tree of components with the DwMessage object at the root.
|
|
|
|
Assemble() builds the string representation of the DwMessage object by
|
|
|
|
traversing the tree and concatenating the strings of the leaf nodes.
|
|
|
|
While every node in the tree is a DwMessageComponent, and therefore has a
|
|
|
|
Parse() and Assemble() member function, you do not have to call these
|
|
|
|
member functions for every node in the tree. The reason is that both of
|
|
|
|
these functions traverse the subtree rooted at the current node. Parse()
|
|
|
|
acts first on the current node, then calls the Parse() member function of
|
|
|
|
its child nodes. Assemble() first calls the Assemble() member functions
|
|
|
|
of a node's child nodes, then concatenates the string representations of
|
|
|
|
its child nodes. Therefore, when you call Parse() or Assemble() for an
|
|
|
|
object of the class DwMessage, Parse() or Assemble() will be called
|
|
|
|
automatically for every component (that is, child node) in the message.
|
|
|
|
|
|
|
|
DwMessageComponent also has one important attribute that you should be aware
|
|
|
|
of. That attribute is an is-modified flag (aka dirty flag), which is
|
|
|
|
cleared whenever Parse() or Assemble() is called, and is set whenever the
|
|
|
|
broken-down representation is modified. To understand how this works,
|
|
|
|
suppose you have just called Parse() on a DwMessage object to create its
|
|
|
|
broken-down representation. If you add a new DwField object (representing a
|
|
|
|
new header field) to the DwHeaders object (representing the header), the
|
|
|
|
is-modified flag will be set for the DwHeaders object, indicating that the
|
|
|
|
string representation of the DwHeaders object will have to be re-assembled
|
|
|
|
from the header fields that it tqcontains. When a node's is-modified flag is
|
|
|
|
set, it also notifies its tqparent node to set its is-modified flag. Thus,
|
|
|
|
when the DwHeaders object's is-modified flag is set, the DwMessage object
|
|
|
|
that is its tqparent will also have its is-modified flag set. That way, when
|
|
|
|
Assemble() is called for the DwMessage object, it will call the Assemble()
|
|
|
|
member function for the DwHeaders object, as required. Notice that the value
|
|
|
|
of having an is-modified flag is that it can purge the tree traversal when
|
|
|
|
the string representation of a message is being assembled.
|
|
|
|
|
|
|
|
One of the first classes you should become familiar with is the DwString
|
|
|
|
class, which handles character strings in MIME++. DwString has been
|
|
|
|
designed to handle very large character strings, so it may be different
|
|
|
|
from string classes in other libraries. Most of the standard C library
|
|
|
|
string functions have DwString counterparts in MIME++. These functions
|
|
|
|
all start with "Dw", and include DwStrcpy(), DwStrcmp(), DwStrcasecmp(),
|
|
|
|
and so on. In addition, the equality operators and assignment operators
|
|
|
|
work as expected. If you have used string classes from other libraries,
|
|
|
|
you will find DwString fairly intuitive.
|
|
|
|
|
|
|
|
The following sections describe how to create, parse, and modify a
|
|
|
|
message. You should also look at the example programs included with the
|
|
|
|
distribution. These example programs are well-commented and use wrapper
|
|
|
|
classes. The wrapper classes BasicMessage, MultipartMessage, and
|
|
|
|
MessageWithAttachments, are designed with three purposes in mind. First,
|
|
|
|
if your requirements are very modest -- say you just want to send a few
|
|
|
|
files as attachments -- then you may find these classes to be adequate for
|
|
|
|
your needs, and you will not have to learn the MIME++ library classes.
|
|
|
|
Second, wrapper classes are the recommended way to use MIME++. You should
|
|
|
|
consider starting with these classes and customizing them for your own
|
|
|
|
application. Using wrapper classes will simplify the use of the MIME++
|
|
|
|
library, but will also help to shield your application from future changes
|
|
|
|
in the MIME++ library. Third, these classes provide excellent examples for
|
|
|
|
how to use the MIME++ library classes.
|
|
|
|
|
|
|
|
The rest of this tutorial focuses on the library classes themselves.
|
|
|
|
|
|
|
|
|
|
|
|
2. Creating a Message
|
|
|
|
|
|
|
|
Creating a message with MIME++ involves instantiating a DwMessage object,
|
|
|
|
setting values for its parts, and assembling the message into its final
|
|
|
|
string representation. The following simple example shows how to
|
|
|
|
accomplish this.
|
|
|
|
|
|
|
|
|
|
|
|
void SendMessage(
|
|
|
|
const char* aTo,
|
|
|
|
const char* aFrom,
|
|
|
|
const char* aSubject,
|
|
|
|
const char* aBody)
|
|
|
|
{
|
|
|
|
// Create an empty message
|
|
|
|
|
|
|
|
DwMessage msg;
|
|
|
|
|
|
|
|
// Set the header fields.
|
|
|
|
// [ Note that a temporary DwString object is created for
|
|
|
|
// the argument for FromString() using the
|
|
|
|
// DwString::DwString(const char*) constructor. ]
|
|
|
|
|
|
|
|
DwHeaders& headers = msg.Headers();
|
|
|
|
headers.MessageId().CreateDefault();
|
|
|
|
headers.Date().FromCalendarTime(time(NULL)); //current date, time
|
|
|
|
headers.To().FromString(aTo);
|
|
|
|
headers.From().FromString(aFrom);
|
|
|
|
headers.Subject().FromString(aSubject);
|
|
|
|
|
|
|
|
// Set the message body
|
|
|
|
|
|
|
|
msg.Body().FromString(aBody);
|
|
|
|
|
|
|
|
// Assemble the message from its parts
|
|
|
|
|
|
|
|
msg.Assemble();
|
|
|
|
|
|
|
|
// Finally, send it. In this example, just print it to the
|
|
|
|
// cout stream.
|
|
|
|
|
|
|
|
cout << msg.AsString();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
In this example, we set the fields 'Message-Id', 'Date', 'To', 'From', and
|
|
|
|
'Subject', which are all documented in RFC-822. The MIME++ class DwHeaders
|
|
|
|
directly supports all header fields documented in RFC-822, RFC-2045, and
|
|
|
|
RFC-1036. To access the field-body for any one these fields, use the
|
|
|
|
member function from DwHeaders that has a name corresponding to the
|
|
|
|
field-name for that field. The correspondence between a field-name and
|
|
|
|
the name of the member function in DwHeaders is consistent: hyphens are
|
|
|
|
dropped and the first character after the hyphen is capitalized. Thus,
|
|
|
|
field-name Content-type in RFC-1521 corresponds to the member function
|
|
|
|
name ContentType. These field-body access functions create an empty field
|
|
|
|
in the headers if that field does not already exist. To check if a
|
|
|
|
particular field exists already, DwHeaders provides member functions
|
|
|
|
HasXxxxx(); for example, HasSender(), HasMimeVersion(), or HasXref()
|
|
|
|
will indicate whether the DwHeaders object has a 'Sender' field, a
|
|
|
|
'MIME-Version' field, or an 'Xref' field, respectively.
|
|
|
|
|
|
|
|
In the example, we used the FromString() member function of
|
|
|
|
DwMessageComponent to set the string representation of the field-bodies.
|
|
|
|
This is the simplest way to set the contents of a DwFieldBody object.
|
|
|
|
Many of the field-bodies also have a broken-down represenation, and it is
|
|
|
|
possible to set the parts of the broken-down representation. Consider, for
|
|
|
|
example, the DwDateTime class, which represents the date-time element of the
|
|
|
|
BNF grammar specified in RFC-822. In the example above, we did not set the
|
|
|
|
string representation -- that would be more difficult and error prone.
|
|
|
|
Instead we set the contents from the time_t value returned from a call to
|
|
|
|
the ANSI C function time(). The DwDateTime class also contains member
|
|
|
|
functions for setting individual attributes. For example, we could have
|
|
|
|
used the following code:
|
|
|
|
|
|
|
|
DwDateTime& date = msg.Headers().Date();
|
|
|
|
time_t t = time(NULL);
|
|
|
|
struct tm stm = *localtime(&t);
|
|
|
|
date.SetYear(stm.tm_year);
|
|
|
|
date.SetMonth(stm.tm_mon);
|
|
|
|
date.SetDay(stm.tm_mday);
|
|
|
|
date.SetHour(stm.tm_hour);
|
|
|
|
date.SetMinute(stm.tm_min);
|
|
|
|
|
|
|
|
|
|
|
|
3. Parsing a Message
|
|
|
|
|
|
|
|
Parsing a received message with MIME++ involves instantiating a DwMessage
|
|
|
|
object, setting its string representation to contain the message, and then
|
|
|
|
calling the Parse() member function of the DwMessage object. The
|
|
|
|
following simple example shows how to accomplish this.
|
|
|
|
|
|
|
|
void ParseMessage(DwString& aMessageStr)
|
|
|
|
{
|
|
|
|
// Create a message object
|
|
|
|
// We can set the message's string representation directly from the
|
|
|
|
// constructor, as in the uncommented version. Or, we can use the
|
|
|
|
// default constructor and set its string representation using
|
|
|
|
// the member function DwMessage::FromString(), as in the
|
|
|
|
// commented version.
|
|
|
|
|
|
|
|
DwMessage msg(aMessageStr);
|
|
|
|
|
|
|
|
// Alternate technique:
|
|
|
|
// DwMessage msg; // Default constructor
|
|
|
|
// msg.FromString(aMessageStr); // Set its string representation
|
|
|
|
|
|
|
|
// Execute the parse method, which will create the broken-down
|
|
|
|
// representation (the tree representation, if you recall)
|
|
|
|
|
|
|
|
msg.Parse();
|
|
|
|
|
|
|
|
// Print some of the header fields, just to show how it's done
|
|
|
|
|
|
|
|
// Date field. First check if the field exists, since
|
|
|
|
// DwHeaders::Date() will create it if is not found.
|
|
|
|
|
|
|
|
if (msg.Headers().HasDate()) {
|
|
|
|
cout << "Date of message is "
|
|
|
|
<< msg.Headers().Date().AsString()
|
|
|
|
<< '\n';
|
|
|
|
}
|
|
|
|
|
|
|
|
// From field. Here we access the broken-down field body, too,
|
|
|
|
// to get the full name (which may be empty), the local part,
|
|
|
|
// and the domain of the first mailbox. (The 'From' field can
|
|
|
|
// have a list of mailboxes).
|
|
|
|
|
|
|
|
if (msg.Headers().HasFrom()) {
|
|
|
|
DwMailboxList& from = msg.Headers().From();
|
|
|
|
cout << "Message is from ";
|
|
|
|
|
|
|
|
// Get first mailbox, then iterate through the list
|
|
|
|
|
|
|
|
int isFirst = 1;
|
|
|
|
DwMailbox* mb = from.FirstMailbox();
|
|
|
|
while (mb) {
|
|
|
|
if (isFirst) {
|
|
|
|
isFirst = 0;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
cout << ", ";
|
|
|
|
}
|
|
|
|
DwString& fullName = mb->FullName();
|
|
|
|
if (fullName != "") {
|
|
|
|
cout << fullName << '\n';
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
// Aptqparently, there is no full name, so use the email
|
|
|
|
// address
|
|
|
|
cout << mb->LocalPart() << '@' << mb->Domain() << '\n';
|
|
|
|
}
|
|
|
|
mb = mb->Next();
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// Finally, print the message body, just to show how the body is
|
|
|
|
// retrieved.
|
|
|
|
|
|
|
|
cout << msg.Body().AsString() << '\n';
|
|
|
|
}
|
|
|
|
|
|
|
|
Once you have parsed the message, you can access any of its parts. The
|
|
|
|
field-bodies of well-known header fields can be accessed by calling member
|
|
|
|
functions of DwHeaders. Some examples follow.
|
|
|
|
|
|
|
|
DwMediaType& contType = msg.Headers().ContentType();
|
|
|
|
DwMechanism& cte = msg.Headers().ContentTransferEncoding();
|
|
|
|
DwDateTime& date = msg.Headers().Date();
|
|
|
|
|
|
|
|
The various subclasses of DwFieldBody, including DwMediaType, DwMechanism,
|
|
|
|
and DwDateTime above, have member functions that allow you to access the parts
|
|
|
|
of the field-body. For example, DwMediaType has member functions to allow
|
|
|
|
you to access its type, subtype, and parameters. If the message is a
|
|
|
|
multipart message, you may access the body parts by calling member
|
|
|
|
functions of the class DwBody. See the example code in multipar.cpp for
|
|
|
|
an example of how to do this.
|
|
|
|
|
|
|
|
|
|
|
|
4. Modifying a Message
|
|
|
|
|
|
|
|
Modifying a message combines the procedures of parsing a message and
|
|
|
|
creating a message. First, parse the message, as explained above. Then
|
|
|
|
set the values of the components -- field-bodies, new fields, new body
|
|
|
|
parts, or what have you -- that you wish to modify. Finally, call the
|
|
|
|
Assemble() member function of the DwMessage object to reassemble the
|
|
|
|
message. You can then access the modified message by calling
|
|
|
|
DwMessage::AsString(). These final steps are the same as those involved
|
|
|
|
in creating a new message.
|
|
|
|
|
|
|
|
|
|
|
|
5. Customizing MIME++ Classes
|
|
|
|
|
|
|
|
MIME++ has been designed to be easily customizable. Typically, you
|
|
|
|
customize C++ library classes through inheritance. MIME++ allows you to
|
|
|
|
create subclasses of most of its library classes in order to change their
|
|
|
|
behavior. MIME++ also includes certain 'hooks', which make it far easier
|
|
|
|
to customize certain parts of the library.
|
|
|
|
|
|
|
|
The most common customization is that of changing the way header fields
|
|
|
|
are dealt with. This could include adding the ability to handle certain
|
|
|
|
non-standard header fields, or to change the way the field-bodies of
|
|
|
|
certain standard header fields are interpreted or parsed. As an example of
|
|
|
|
the former customization, you may want to add the 'X-status' field or
|
|
|
|
'X-sender' field to your messages. As an example of the latter, you may
|
|
|
|
want to change DwMediaType so that it will handle other MIME subtypes.
|
|
|
|
|
|
|
|
Let's begin with the latter situation -- that of subclassing DwMediaType.
|
|
|
|
Obviously, you will have to become familiar with DwMediaType and its
|
|
|
|
superclasses before you change its behavior. Then, at a minimum, you will
|
|
|
|
want to provide your own implementation of the virtual member functions
|
|
|
|
Parse() and Assemble(). Once you feel comfortable with the behavior of
|
|
|
|
the behavior of your new class -- call it MyMediaType -- you will have to
|
|
|
|
take the right steps to ensure that the MIME++ library internal routines
|
|
|
|
will create objects of type MyMediaType, and not DwMediaType. There are
|
|
|
|
three such steps.
|
|
|
|
|
|
|
|
First, define a function NewMyMediaType(), matching the prototype
|
|
|
|
|
|
|
|
DwMediaType* NewMyMediaType(
|
|
|
|
const DwString& aStr,
|
|
|
|
DwMessage* aParent)
|
|
|
|
|
|
|
|
that creates a new instance of MyMediaType and returns it. Set the static
|
|
|
|
data member DwMediaType::sNewMediaType to point to this function.
|
|
|
|
DwMediaType::sNewMediaType is normally NULL, meaning that no user-defined
|
|
|
|
function is available. When you set this static data member, however,
|
|
|
|
MIME++'s internal routines will call your own function, and will therefore
|
|
|
|
be able to create instances of your subclass.
|
|
|
|
|
|
|
|
Second, make sure you have reimplemented the virtual function
|
|
|
|
DwMediaType::Clone() to return a clone of your own subclassed object.
|
|
|
|
Clone() serves as a 'virtual constructor'. (See the discussion of virtual
|
|
|
|
constructors in Stroustrup's _The C++ Programming Language_, 2nd Ed).
|
|
|
|
|
|
|
|
Third, you should define a function CreateFieldBody(), matching the
|
|
|
|
prototype
|
|
|
|
|
|
|
|
DwFieldBody* CreateFieldBody(
|
|
|
|
const DwString& aFieldName,
|
|
|
|
const DwString& aFieldBody,
|
|
|
|
DwMessageComponent* aParent)
|
|
|
|
|
|
|
|
that returns an object of a subclass of DwFieldBody. (DwFieldBody is a
|
|
|
|
superclass of MyMediaType). CreateFieldBody() is similar to the
|
|
|
|
NewMyMediaType() function already described, except that its first
|
|
|
|
argument supplies the field-name for the particular field currently being
|
|
|
|
handled by MIME++. CreateFieldBody() should examine the field-name,
|
|
|
|
create an object of the appropriate subclass of DwFieldBody, and return a
|
|
|
|
pointer to the object. In this particular case, you need to make sure
|
|
|
|
that when the field-name is 'Content-Type' you return an object of the
|
|
|
|
class MyMediaType. Set the hook for CreateFieldBody() setting the static
|
|
|
|
data member DwField::sCreateFieldBody to point to your CreateFieldBody()
|
|
|
|
function. DwField::sCreateFieldBody is normally NULL when no user
|
|
|
|
function is provided.
|
|
|
|
|
|
|
|
These three steps are sufficient to ensure that your subclass of
|
|
|
|
DwMediaType is integrated with the other MIME++ classes.
|
|
|
|
|
|
|
|
The other customization task mentioned above is that of adding support for
|
|
|
|
a non-standard header field. There is a simple way to do this, and a way
|
|
|
|
that involves creating a subclass of DwHeaders. You can access any header
|
|
|
|
field by calling DwHeaders's member functions. In fact, you can iterate
|
|
|
|
over all the header fields if you would like. Therefore, the really
|
|
|
|
simple way is just to not change anything and just use existing member
|
|
|
|
functions. The relevant functions include DwHeaders::HasField(), which will
|
|
|
|
return a boolean value indicating if the header has the specified field,
|
|
|
|
and DwHeaders::FieldBody(), which will return the DwFieldBody object
|
|
|
|
associated with a specified field. [ Note that DwHeaders::FieldBody() will
|
|
|
|
create a field if it is not found. ] The default DwFieldBody subclass,
|
|
|
|
which applies to all header fields not recognized by MIME++, is DwText,
|
|
|
|
which is suitable for the unstructured field-bodies described in RFC-822
|
|
|
|
such as 'Subject', 'Comments', and so on. If a DwText object is suitable
|
|
|
|
for your non-standard header field, then you don't have to do anything at all.
|
|
|
|
Suppose, however, that you want an object of your own subclass of
|
|
|
|
DwFieldBody, say StatusFieldBody, to be attached to the 'X-status' field.
|
|
|
|
In this case, you will need to set the hook DwField::sCreateFieldBody as
|
|
|
|
discussed above. Your CreateFieldBody() function should return an
|
|
|
|
instance of StatusFieldBody whenever the field-name is 'X-status'.
|
|
|
|
|
|
|
|
Finally, while you can access any header field using DwHeaders's member
|
|
|
|
functions, you may want to create your own subclass of DwHeaders for some
|
|
|
|
reason or other -- maybe to add a convenience function to access the
|
|
|
|
'X-status' header field. To ensure that your new class is integrated with
|
|
|
|
the library routines, you basically follow steps 1 and 2 above for
|
|
|
|
subclassing DwFieldBody. First, define a function NewMyHeaders() and set the
|
|
|
|
static data member DwHeaders::sNewHeaders to point to your function. Second,
|
|
|
|
make sure you have reimplemented the virtual function DwHeaders::Clone() to
|
|
|
|
return an instance of your subclass. Step 3 for subclassing DwFieldBody
|
|
|
|
does not apply when subclassing DwHeaders.
|
|
|
|
|
|
|
|
|
|
|
|
6. Getting Help
|
|
|
|
|
|
|
|
I will try to help anyone who needs help specific to MIME++. I won't try
|
|
|
|
to answer general questions about C++ that could be answered by any C++
|
|
|
|
expert. Bug reports will receive the highest priority. Other questions
|
|
|
|
about how to do something I will try to answer in time, but I ask for your
|
|
|
|
patience. If you have any comments -- perhaps maybe you know of a better
|
|
|
|
way to do something -- please send them. My preferred email is
|
|
|
|
dwsauder@fwb.gulf.net, but dwsauder@tasc.com is also acceptable.
|
|
|
|
|
|
|
|
Good luck!
|
|
|
|
|