|
From: <jm...@us...> - 2005-09-23 06:38:10
|
Update of /cvsroot/struts/dialogs/war In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv17798/war Added Files: error.html filelist.txt index.jsp strutsdialogs-power.gif tour.html Log Message: Added MailReader Demo; Added component RuleSet --- NEW FILE: error.html --- <html> <p>ACCESS DENIED</p> </html> --- NEW FILE: filelist.txt --- Struts Dialogs samples: ----------------------- index.html - contains links to all samples MailReader Demo Application: ---------------------------- index.jsp - redirects to Home.do error.html - displays "ACCESS DENIED" if JSP page accessed directly tour.html - displays tour documentation Common files: ------------- strutsdialogs-power.gif - "Powered by Struts Dialogs" logo --- NEW FILE: index.jsp --- <%@ taglib uri="http://struts.apache.org/tags-logic" prefix="logic" %> <%@ page session="false" %> <%-- Redirect default requests to Welcome action. By using a redirect, the user-agent will change address to match the path of our Welcome action. --%> <logic:redirect action="/Home"/> --- NEW FILE: strutsdialogs-power.gif --- (This appears to be a binary file; contents omitted.) --- NEW FILE: tour.html --- <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <META http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Struts Dialogs: Mail Reader</title> <link type="text/css" href="../skin/page.css" rel="stylesheet"> </head> <body text="#000000" bgcolor="#FFFFFF"> <h1>Struts Dialogs: Mail Reader</h1> <a name="N1000C"></a><a name="overview"></a> <h3>Overview</h3> <div style="margin-left: 0 ; border: 2px"> <p>This document is based on Ted Husted's documentation for original MailReader application. For the sake of readability the modifications of the original text are not shown. The non-relevant portions of original document like plugins, database, Commons Validator are removed, please refer to original MailReader documentation.</p> <p>The premise of the MailReader is that it is the first iteration of a portal application. This version allows users to register themselves and to create and maintain a set of accounts with various mail servers. When completed, the application would let users read mail from their accounts. This sample application does not allow sending and receiving of real emails, it collects user information and mail addresses only.</p> <p>The MailReader application demonstrates registering with an application, logging into an application, maintaining a master record, and maintaining child records. This document walks through the JSP pages, Struts Java classes, and Struts configuration elements needed to do these things.</p> <p>The walkthrough starts with how the initial page is displayed, and then steps through logging in, adding and editing subscriptions, and creating a new registration.</p> </div> <a name="N1001F"></a><a name="actions"></a> <h3>Actions</h3> <div style="margin-left: 0 ; border: 2px"> <p>The goal of Struts is to simplify development of web applications. It is facilitated by means of custom requests handlers called <em>actions</em>. You may think of action as of mini-servlet.</p> <p>An action performs the following functions:</p> <ul> <li>receives a request from browser;</li> <li>interprets request parameters;</li> <li>updates domain objects if needed;</li> <li>generates dynamic output;</li> </ul> <p>The actions are listed in a configuration file and identify <em>web resources</em>. Actions can generate different output depending on state of web resource and on request parameters.</p> <p>An accepted practice in Struts is to never link directly to server pages, but only to the actions. In Struts, the role of server page is reduced to data-aware HTML, used exclusively as presentation technology.</p> <p> <strong>"Link actions not pages."</strong> </p> <p>By linking to actions, developers can "rewire" an application without editing the server pages.</p> <p>The picture below shows the structure of MailReader application, developed with Struts Dialogs. It is slightly changed from original MailReader for simplicity and clarity. Notice, that different pages are not linked to each other, making up a "flow". Instead, pages are organized as a table, where rows correspond to web resources, and columns correspond to state of these web resources.</p> <div align="center"> <img class="figure" alt="MailReader Matrix" src="images/mailreader-matrix.gif"></div> <p>MailReader has four distinct web resources: <strong>Home</strong>, <strong>Accounts</strong>, <strong>Login</strong> and <strong>Subscriptions</strong>. Each resource behaves differently depending on state. Theare are two application-wide states: "Not Logged In" and "Logged In" that indicate whether a user is logged in or not.</p> <p> <strong>Home</strong>, <strong>Accounts</strong> and <strong>Login</strong> resources exist in both states, while <strong>Subscriptions</strong> resource has meaningful output only for "Logged In" state, because Subscriptions resorce displays email subscriptions for currently logged-in user.</p> <p>Each web resource is defined with one Struts action class and one or more JSP pages. For example, Home resource defines only one JSP page, which is modified at runtime, while Login resource defines two distinct pages, each for one state.</p> <p>Nice thing about stateful resources is that they produce valid output whenever they are navigated to. There is no need to establish a strict flow between resources or their pages.</p> </div> <a name="N10078"></a><a name="indexpage"></a> <h3>Index Page</h3> <div style="margin-left: 0 ; border: 2px"> <p>Struts allows developers to manage an application through "virtual pages" called actions. A web application, like any other web site, can specify a list of welcome pages. When you open a web application without specifying a particular page, a welcome page is used by default. Unfortunately, actions cannot be specified as a welcome page. So, in the case of a welcome page, how do we follow the Struts best practice of navigating through actions rather than pages?</p> <p>One solution is to use a server page to "bootstrap" a Struts action. A Java web application recognizes the idea of "forwarding" from one action (or page) to another action (or page). We can register the usual <span class="codefrag">index.jsp</span> as the welcome page and have it forward redirect to a "Home" action. Here's the MailReader's <span class="codefrag">index.jsp</span>:</p> <pre class="code"> <%@ taglib uri="/tags/struts-logic" prefix="logic" %> <%@ page session="false"> <logic:redirect action="/Home"/> </pre> <p>At the top of the page, we import the "struts-logic" JSP tag library. (Again, see the <a href="http://jakarta.apache.org/struts/userGuide/preface.html">Preface to the Struts User Guide</a> for more about the technologies underlying Struts.) We also tell the server to wait creating a session. The page itself consists of a single tag that redirects to the "Welcome" action. The tag inserts the actual web address for the redirect when the page is rendered. But, where does the tag find the actual address to insert?</p> <p>The actions, along with other Struts components, are registered through one or more Struts configuration files. The configuration files are written as XML documents and processed when the application starts.</p> </div> <!-- ********************** --> <h2>Home component</h2> <a name="N1000C"></a><a name="overview"></a> <h3>Overview</h3> <div style="margin-left: 0 ; border: 2px"> <p> <strong>Home</strong> component carries out three functions:</p> <ul> <li>Startup menu</li> <li>Main menu</li> <li>Language selection</li> </ul> <p>Home component is controlled by one action (HomeAction) and has one view (home.jsp). The <span class="codefrag">home.jsp</span> page displays either startup menu or main menu depending on user's logged-in status.</p> <p>Startup menu is shown to a non-logged-in user and allows either to log in or to create a new user account.</p> <div align="center"> <img class="figure" alt="MailReader Startup Menu" src="images/mailreader-home-welcome.gif"></div> <p></p> <p>Main menu is shown to a logged-in user and allows to review and update user account information, to manage email subscriptions and to log off the application.</p> <div align="center"> <img class="figure" alt="MailReader Main Menu" src="images/mailreader-home-mainmenu.gif"></div> </div> <a name="N1003A"></a><a name="home-jsp"></a> <h3>home.jsp</h3> <div style="margin-left: 0 ; border: 2px"> <p>Let us first design the <span class="codefrag">home.jsp</span> page, then we will define action class and wire events to event handles. The <span class="codefrag">home.jsp</span> page displays different menu options depending on user's status.</p> <pre class="code"> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %> <%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %> <%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %> <html> <head> <title><bean:message key="index.title"/></title> <link rel="stylesheet" type="text/css" href="base.css" /> </head> <body> <h3><bean:message key="index.heading"/></h3> <!-- Signup and login options for not logged-in user --> <logic:notPresent name="user" scope="session"> <ul> <li> <html:link action="<strong>/Home?DIALOG-EVENT-SIGNUP</strong>"> <bean:message key="index.registration"/></html:link> </li> <li> <html:link action="<strong>/Home?DIALOG-EVENT-LOGON</strong>"> <bean:message key="index.logon"/> </html:link> </li> </ul> </logic:notPresent> <!-- Account modification and subscription list for logged-in user --> <logic:present name="user" scope="session"> <ul> <li> <html:link action="<strong>/Home?DIALOG-EVENT-ACCUPDATE</strong>"> <bean:message key="index.updateaccount"/> </html:link> </li> <li> <html:link action="<strong>/Home?DIALOG-EVENT-SUBSCRIPTIONS</strong>"> <bean:message key="index.managesubscriptions"/> </html:link> </li> <li> <html:link action="<strong>/Logon</strong>"> <bean:message key="index.showuserinfo"/> </html:link> </li> <li> <html:link action="<strong>/Logon?DIALOG-EVENT-LOGOFF</strong>"> <bean:message key="index.logoff"/> </html:link> </li> </ul> </logic:present> <!-- Language selection --> <h3><bean:message key="index.langopts"/></h3> <ul> <li> <html:link action="<strong>/Home?DIALOG-EVENT-LOCALE&language=en</strong>"> <bean:message key="index.langenglish"/> </html:link> </li> <li> <html:link action="<strong>/Home?DIALOG-EVENT-LOCALE&language=ru</strong>" useLocalEncoding="true"> <bean:message key="index.langrussian"/> </html:link> </li> </ul> <hr/> <p><html:img bundle="alternate" pageKey="strutsdialogs.logo.path" altKey="strutsdialogs.logo.alt"/></p> </body> </html> </pre> <p>The <span class="codefrag">home.jsp</span> page has three sections. The language selection section is always displayed. The startup menu is displayed when a user is not logged in; it is wrapped into <span class="codefrag"><logic:notPresent name="user" scope="session"></span> element. The main menu is displayed to logged in user; it is wrapped into <span class="codefrag"><logic:present name="user" scope="session"></span> element. This works because when a user logs in, the "user" bean with user's account information is saved in the session.</p> </div> <a name="N10073"></a><a name="events"></a> <h3>Login state and events</h3> <div style="margin-left: 0 ; border: 2px"> <p>The links in each section of <span class="codefrag">home.jsp</span> page generate events. An event is a HTTP request identified with dialog event parameter. By default, dialog event parameter name should start with "DIALOG-EVENT", as seen in the code above. Of course, it is possible to pass additional request parameters along, for example to change interface language to Russian: <span class="codefrag">/Home?DIALOG-EVENT-LOCALE&language=ru</span> </p> <p>The recommended practice is to send all events to a parent action, in case of <span class="codefrag">home.jsp</span> page this would be HomeAction class. For example, <span class="codefrag">/Home?DIALOG-EVENT-LOGON</span> generates logon event, while <span class="codefrag">/Home?DIALOG-EVENT-SIGNUP</span> generates signup event. The benefit of sending all page events to a parent action is centralized processing and cleaner application structure.</p> <p>On the other hand, nothing prevents from sending an event to a different action class, like you see in the case of logging user off: <span class="codefrag">/Logon?DIALOG-EVENT-LOGOFF</span>. Here the logoff event is sent directly to Logon action.</p> </div> <a name="N10094"></a><a name="homeaction"></a> <h3>Home action</h3> <div style="margin-left: 0 ; border: 2px"> <p>To properly handle input events and output page rendering, the home action must subclass a <a href="dialogaction.html">DialogAction</a> class. Then request events are mapped to method handlers, <span class="codefrag">getInitKey</span> returns "DIALOG-EVENT" by default:</p> <pre class="code"> public final class HomeAction extends DialogAction { ... protected Map getKeyMethodMap() { Map methodMap = new HashMap(); methodMap.put(getInitKey()+"-LOGON", "onLogon"); methodMap.put(getInitKey()+"-SIGNUP", "onAccountSignup"); methodMap.put(getInitKey()+"-ACCUPDATE", "onAccountUpdate"); methodMap.put(getInitKey()+"-LOCALE", "onLocale"); methodMap.put(getInitKey()+"-SUBSCRIPTIONS", "onSubscriptions"); return methodMap; } ... } </pre> <p>Then event handlers are implemented. They must have the same signature as <span class="codefrag">Action.execute</span> method. For example, this is how account update handler is coded:</p> <pre class="code"> public ActionForward onAccountUpdate(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { return mapping.findForward("accupdate"); } </pre> <p>Method <span class="codefrag">onAccountUpdate</span> does not do much. It simply forwards the request to another resource called "accupdate".</p> <p>The hanlder of language-change event is more elaborate. It obtains the desired language either from request parameter or from request header, sets the "locale" key in the session and redisplays <span class="codefrag">home.jsp</span> page:</p> <pre class="code"> public ActionForward onLocale(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { String language = request.getParameter("language"); String country = request.getParameter("country"); Locale locale = getLocale(request); if ((!isBlank(language)) && (!isBlank(country))) { locale = new Locale(language, country); } else if (!isBlank(language)) { locale = new Locale(language, ""); } // Set current locale HttpSession session = request.getSession(); session.setAttribute(Globals.LOCALE_KEY, locale); // Redisplay home page return EventForward.DIALOG_RELOAD; } </pre> <p>See the description for <a href="dialogaction.html">DialogAction</a> class for in-depth explanation of how Struts Dialogs components process input events and generate output.</p> </div> <a name="N100CD"></a><a name="config"></a> <h3>Configuration of Home component</h3> <div style="margin-left: 0 ; border: 2px"> <p>Follows the configuration of Home component in <span class="codefrag">struts-config.xml</span> file. The example below uses new <strong>component</strong>, <strong>event</strong> and <strong>render</strong> elements, introduced in Struts Dialogs 1.23:</p> <pre class="code"> <action-mappings> ... <!-- Home page. Handles welcome menu, main menu and language selection --> <component path = "/Home" view = "/home.jsp" type = "net.jspcontrols.mailreader.HomeAction"> <event name = "logon" path = "/Logon.do?DIALOG-EVENT-INIT"/> <event name = "signup" path = "/Registration.do?DIALOG-EVENT-CREATE"/> <event name = "accupdate" path = "/Registration.do?DIALOG-EVENT-UPDATE"/> <event name = "subscriptions" path = "/Subscriptions.do?DIALOG-EVENT-INIT"/> <render name = "failure" path = "/error.jsp" /> </component> ... </action-mappings> </pre> <p>The same component can be configured using standard Struts <strong>action</strong> and <strong>forward</strong> elements, though with less clarity:</p> <pre class="code"> <action-mappings> ... <action path = "/Home" scope = "session" validate = "false" type = "net.jspcontrols.mailreader.HomeAction"> <forward name = "logon" path = "/Logon.do?DIALOG-EVENT-INIT" redirect = "true"/> <forward name = "signup" path = "/Registration.do?DIALOG-EVENT-CREATE" redirect = "true"/> <forward name = "accupdate" path = "/Registration.do?DIALOG-EVENT-UPDATE" redirect = "true"/> <forward name = "subscriptions" path = "/Subscriptions.do?DIALOG-EVENT-INIT" redirect = "true"/> <forward name = "DIALOG-VIEW" path = "/home.jsp"/> <forward name = "failure" path = "/error.jsp"/> </action> ... </action-mappings> </pre> <p>HomeAction has only one corresponding view, <span class="codefrag">home.jsp</span>, therefore HomeAction takes advantage of "view" attribute of "component" element, which defines the view for a component. If traditional syntax for action configuration is used, the view should be defined as result of a forward element with "DIALOG-VIEW" name. This is one of the default view mapping names, that is used by <a href="dialogaction.html">DialogAction</a>.</p> <p>Similarly, <span class="codefrag"><event></span> element is just the same as <span class="codefrag"><forward ... redirect = "true"></span>, while <span class="codefrag"><render></span> element is the same as <span class="codefrag"><forward ... redirect = "false"></span>. Also, <span class="codefrag"><component></span> element has different defaults, like turned off validation and session scope for form beans.</p> <p>To use new syntax in <span class="codefrag">struts-config.xml</span>, you need to use Struts Dialogs 1.23 and to add new RuleSet object to <span class="codefrag">web.xml</span> file:</p> <pre class="code"> <init-param> <param-name>rulesets</param-name> <param-value>net.jspcontrols.dialogs.actions.DialogRuleSet</param-value> </init-param> </pre> <p>If you have config validation turned on, then you need to have a proper DTD file <span class="codefrag">struts-config_1_2_dialog.dtd</span> for updated <span class="codefrag">struts-config.xml</span> file. If you place DTD file in WEB-INF directory, then you can refer to it using the following doctype in the struts-config.xml file:</p> <pre class="code"> <!DOCTYPE struts-config SYSTEM "struts-config_1_2_dialog.dtd"> </pre> <p>The MailReader application retains a list of users along with their email accounts. The application stores this information in a database. If the application can't connect to the database, the application can't do its job. So before displaying the home page, the class checks to see if the database is available. The MailReader is also an internationalized application. So, the WelcomeAction checks to see if the message resources are available too. If both resources are available, the class displays home page. Otherwise, it forwards to the "failure" path so that the appropriate error messages can be displayed:</p> <pre class="code"> public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { // Setup message array in case there are errors ArrayList messages = new ArrayList(); // Confirm message resources loaded MessageResources resources = getResources(request); if (resources == null) { messages.add(Constants.ERROR_MESSAGES_NOT_LOADED); } // Confirm database loaded UserDatabase userDatabase = getUserDatabase(request); if (userDatabase == null) { messages.add(Constants.ERROR_DATABASE_NOT_LOADED); } // If there were errors, queue plain error messages to the request, // and forward to boot failure page. Notice: no redirection here. if (messages.size() > 0) { request.setAttribute(Constants.ERROR_KEY,messages); mapping.findForward(Constants.FAILURE); } // Messages and database initialized successfully, let DialogAction // handle input and output. return super.execute(mapping, form, request, response); } </pre> </div> <!-- <section id="database"> <title>MemoryDatabasePlugIn.java</title> <p>The database is exposed to the application as an object stored in application scope. The database object is based on an interface. Different implementations of the database could be loaded without changing the rest of the application. But how is the database object loaded in the first place?</p> <p>One section of the Struts configuration is devoted to "PlugIns". When a Struts application loads, it also loads whatever PlugIns are specified in its configuration. The PlugIn interface is quite simple, and you can use PlugIns to do anything that might need to be done when your application loads. The PlugIn is also notified when the application shuts down, so you can release any allocated resources.</p> <source> <plug-in className="org.apache.struts.webapp.example.memory.MemoryDatabasePlugIn"> <set-property property="pathname" value="/WEB-INF/database.xml"/> </plug-in> </source> <p>By default, the MailReader application loads a "MemoryDatabase" implementation of the UserDatabase. MemoryDatabase stores the database contents as a XML document, which is parsed by the Digester and loaded as a set of nested hashtables. The outer table is the list of user objects, each of which has its own inner hashtable of subscriptions. When you register, a user object is stored in this hashtable ... and when you login, the user object is stored within the session context.</p> <p>The database comes seeded with a sample user. If you check the database.xml file under WEB-INF, you'll see the sample user described as:</p> <source> <user username="user" fromAddress="Joh...@so..." fullName="John Q. User" password="pass"> <subscription host="mail.hotmail.com" autoConnect="false" password="bar" type="pop3" username="user1234"> </subscription> <subscription host="mail.yahoo.com" autoConnect="false" password="foo" type="imap" username="jquser"> </subscription> </user> </source> <p>This creates a registration record for "John Q. User", with the detail for his hotmail account (or "subscription").</p> </section> <section id="messageresources"> <title>MessageResources.properties</title> <p>Another section of the Struts configuration loads the message resources for the application. If you change a message in the resource, and then reload the application, the change will appear throughout the application. If you provide message resources for additional locales, you can internationalize your application.</p> <source> <message-resources parameter="org.apache.struts.webapp.example.MessageResources" /> </source> <p>This is a standard properties text file. Here are the entries used by the welcome page:</p> <source> index.heading=MailReader Demonstration Application Options index.logon=Log on to the MailReader Demonstration Application index.registration=Register with the MailReader Demonstration Application index.title=MailReader Demonstration Application (Struts Dialogs) index.tour=A Walking Tour of the MailReader Demonstration Application </source> <p>The MailReader application uses a second set of message resources for non-text elements. The "key" element can be used to access this resource bundle rather than the default bundle.</p> <source> <message-resources parameter="org.apache.struts.webapp.example.AlternateMessageResources" key="alternate" /> </source> </section> --> <!-- ************************** --> <h2>Logon component</h2> <p>Login component is controlled by one action (LoginAction) and has two views: <span class="codefrag">login.jsp</span> and <span class="codefrag">logout.jsp</span>.</p> <p> <span class="codefrag">login.jsp</span> is shown to a non-logged-in user and allows either to log in using existing user account.</p> <div align="center"> <img class="figure" alt="MailReader Login" src="images/mailreader-login.gif"></div> <p></p> <p> <span class="codefrag">logout.jsp</span> is shown to a logged-in user and allows to check current user's name and to log off the applicaiton.</p> <div align="center"> <img class="figure" alt="MailReader Logout" src="images/mailreader-logout.gif"></div> </div> <a name="N10041"></a><a name="loginaction"></a> <h3>Logon action</h3> <div style="margin-left: 0 ; border: 2px"> <p>MailReader's <span class="codefrag">LoginAction</span> class does not differ much from standard <a href="dialogaction-logincomponentsample.html">Login Component</a>. The only difference that instead of saving a name of logged-in user in the session, it saves the whole User object, which contains additional account information. Another difference is that <span class="codefrag">LoginAction</span> uses dynabean to store user's name and password, and Commons Validator to check input values.</p> <pre class="code"> public ActionForward onLogon (ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { // Validate user credentials, use Commons Validator ActionMessages errors = form.validate(mapping, request); // Local variables UserDatabase database = getUserDatabase(request); String username = (String) PropertyUtils.getSimpleProperty(form, "username"); String password = (String) PropertyUtils.getSimpleProperty(form, "password"); // Retrieve user from database if (errors.isEmpty()) { // Load user; if not found, errors will be set User user = getUser(database, username, password, errors); // If user not found, this effectively logs out current user SaveUser(request, user); } // Errors found; reload dialog and display errors if (!errors.isEmpty()) { this.saveDialogErrors(request.getSession(), errors); return EventForward.DIALOG_RELOAD; // Successfully logged in; navigate to home page } else { return mapping.findForward("success"); } } </pre> </div> <a name="N1005A"></a><a name="loginpages"></a> <h3>Logon pages</h3> <div style="margin-left: 0 ; border: 2px"> <p>Logon component has two pages. <span class="codefrag">login.jsp</span> is shown to a non-logged-in user and allows either to log in using existing user account. <span class="codefrag">logout.jsp</span> is shown to a logged-in user and allows to check current user's name and to log off the application. The following code selects the appropriate page depending on application state:</p> <pre class="code"> public ActionForward getDialogView(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { // Mark response as non-cachable setNoCache(response); // Display page, corresponding to login state. HttpSession session = request.getSession(); if (session.getAttribute(Constants.USER_KEY) == null) { return mapping.findForward("notloggedin"); } else { return mapping.findForward("loggedin"); } } </pre> </div> <!- ^^^^^^^^^^^^^^ --> <h2>Subscriptions component</h2> <p>Subscriptions component is controlled by one action (SubscriptionAction.java) and has two views: <span class="codefrag">subscriptions.jsp</span> and <span class="codefrag">subscription.jsp</span>.</p> <p> <span class="codefrag">subscriptions.jsp</span> displays a list of email subscriptions. It is shown to a logged-in user, if there is no <em>current</em> subscription.</p> <div align="center"> <img class="figure" alt="MailReader Subscriptions" src="images/mailreader-subscriptions.gif"></div> <p></p> <p>If current subscription exists, <span class="codefrag">subscription.jsp</span> displays it to a logged-in user. It can be either a new subscription...</p> <div align="center"> <img class="figure" alt="MailReader Create Subscription" src="images/mailreader-subscription-create.gif"></div> <p>...or an existing subscription:</p> <div align="center"> <img class="figure"... [truncated message content] |