From 052ce75286477d1a3b66427540063c366e30ee91 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Wed, 20 Apr 2022 10:30:01 +0200 Subject: [PATCH 1/6] fixed character encoding bug --- pom.xml | 2 +- src/main/java/de/srsoftware/widerhall/web/TemplateServlet.java | 2 ++ src/main/java/de/srsoftware/widerhall/web/Web.java | 2 -- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index ac54f28..131af62 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.example Widerhall - 0.0.20 + 0.0.21 diff --git a/src/main/java/de/srsoftware/widerhall/web/TemplateServlet.java b/src/main/java/de/srsoftware/widerhall/web/TemplateServlet.java index 79b24a7..4c69da2 100644 --- a/src/main/java/de/srsoftware/widerhall/web/TemplateServlet.java +++ b/src/main/java/de/srsoftware/widerhall/web/TemplateServlet.java @@ -8,6 +8,7 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.Map; @@ -40,6 +41,7 @@ public abstract class TemplateServlet extends HttpServlet { var template = templates.getInstanceOf(path); if (template != null){ try { + resp.setCharacterEncoding("UTF-8"); template.add("data",data); resp.getWriter().println(template.render()); return null; diff --git a/src/main/java/de/srsoftware/widerhall/web/Web.java b/src/main/java/de/srsoftware/widerhall/web/Web.java index 17914e4..67dbf76 100644 --- a/src/main/java/de/srsoftware/widerhall/web/Web.java +++ b/src/main/java/de/srsoftware/widerhall/web/Web.java @@ -288,7 +288,6 @@ public class Web extends TemplateServlet { } if (!error){ - var dummy = req.getParameterMap(); try { list.forwardFrom(Util.getCheckbox(req, "forward_from")); list.forwardAttached(Util.getCheckbox(req, "forward_attached")); @@ -297,7 +296,6 @@ public class Web extends TemplateServlet { LOG.warn("Failed to update MailingList:",e); data.put(ERROR,t("Failed to update MailingList!")); } - LOG.debug("params: {}",dummy); } return loadTemplate(INSPECT,data,resp); From 523e2fc432a49ed272dced08c8bbc88d3e18b1bc Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Wed, 20 Apr 2022 11:49:23 +0200 Subject: [PATCH 2/6] first working version --- pom.xml | 2 +- .../srsoftware/widerhall/data/ListMember.java | 4 ++ .../widerhall/data/MailingList.java | 52 +++++++++++++++---- .../srsoftware/widerhall/mail/SmtpClient.java | 41 +++++++++++---- .../de/srsoftware/widerhall/web/Rest.java | 2 + .../java/de/srsoftware/widerhall/web/Web.java | 2 + static/templates/inspect.st | 8 +++ static/templates/js.st | 2 + 8 files changed, 93 insertions(+), 20 deletions(-) diff --git a/pom.xml b/pom.xml index 131af62..180b480 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.example Widerhall - 0.0.21 + 0.1.1 diff --git a/src/main/java/de/srsoftware/widerhall/data/ListMember.java b/src/main/java/de/srsoftware/widerhall/data/ListMember.java index 1aa4897..61807ea 100644 --- a/src/main/java/de/srsoftware/widerhall/data/ListMember.java +++ b/src/main/java/de/srsoftware/widerhall/data/ListMember.java @@ -136,6 +136,10 @@ public class ListMember { } + public boolean isOwner(){ + return hasState(STATE_OWNER); + } + /** * return a set of list emails of MailingLists owned by the given user * @param user diff --git a/src/main/java/de/srsoftware/widerhall/data/MailingList.java b/src/main/java/de/srsoftware/widerhall/data/MailingList.java index 708d855..ed71b03 100644 --- a/src/main/java/de/srsoftware/widerhall/data/MailingList.java +++ b/src/main/java/de/srsoftware/widerhall/data/MailingList.java @@ -40,8 +40,11 @@ public class MailingList implements MessageHandler { private static final int STATE_PUBLIC = 2; public static final int STATE_FORWARD_FROM = 4; public static final int STATE_FORWARD_ATTACHED = 8; + public static final int STATE_HIDE_RECEIVERS = 16; + public static final int STATE_REPLY_TO_LIST = 32; private static final int VISIBLE = 1; private static final int HIDDEN = 0; + private static final int DEFAULT_STATE = STATE_PENDING|STATE_HIDE_RECEIVERS; private final String name; private final String email; private int state; @@ -88,7 +91,7 @@ public class MailingList implements MessageHandler { * @throws SQLException */ public static MailingList create(String email, String name, String imapHost, int imapPort, String imapUser, String imapPass, String inbox, String smtpHost, int smtpPort, String smtpUser, String smtpPass) throws SQLException { - return new MailingList(email, name, imapHost, imapPort, imapUser, imapPass, inbox, smtpHost, smtpPort, smtpUser, smtpPass, STATE_PENDING).save(); + return new MailingList(email, name, imapHost, imapPort, imapUser, imapPass, inbox, smtpHost, smtpPort, smtpUser, smtpPass, DEFAULT_STATE).save(); } /** @@ -143,9 +146,17 @@ public class MailingList implements MessageHandler { private void forward(Message message) throws MessagingException { try { - var emails = members().stream().map(ListMember::user).map(User::email).toList(); - String sender = (state & STATE_FORWARD_FROM) > 0 ? message.getFrom()[0].toString() : email(); - smtp.bccForward(sender,message,emails); + String newSender = !hasState(STATE_FORWARD_FROM) ? email() : null; + var receivers = members() + .stream() + .map(ListMember::user) + .map(User::email) + .toList(); + var subject = message.getSubject(); + // TODO: remove '(from …)' from subject + if (!subject.contains(stamp())) subject = stamp()+" "+subject; + var replyTo = (newSender == null && hasState(STATE_REPLY_TO_LIST)) ? email() : null; + smtp.forward(newSender,receivers,message,subject,hasState(STATE_FORWARD_ATTACHED),hasState(STATE_HIDE_RECEIVERS),replyTo); } catch (SQLException e) { LOG.error("Failed to read list members of {} from database. Cannot forward message!",email(),e); } @@ -194,6 +205,10 @@ public class MailingList implements MessageHandler { setFlag(STATE_PUBLIC,!hide); } + public void hideReceivers(boolean hide) throws SQLException { + setFlag(STATE_HIDE_RECEIVERS,hide); + } + /** * test, whether the current ML is subscribable by a given user * @param user @@ -235,7 +250,7 @@ public class MailingList implements MessageHandler { public boolean mayBeAlteredBy(User user) { if (user.hashPermission(PERMISSION_ADMIN)) return true; try { - if (ListMember.load(this,user).hasState(ListMember.STATE_OWNER)) return true; + if (ListMember.load(this,user).isOwner()) return true; } catch (SQLException e) { LOG.debug("Error loading list member for ({}, {})",user.email(),email()); } @@ -245,7 +260,7 @@ public class MailingList implements MessageHandler { public boolean mayBeTestedBy(User user) { if (user.hashPermission(PERMISSION_ADMIN)) return true; try { - if (ListMember.load(this,user).hasState(ListMember.STATE_OWNER)) return true; + if (ListMember.load(this,user).isOwner()) return true; } catch (SQLException e) { LOG.debug("Error loading list member for ({}, {})",user.email(),email()); } @@ -259,7 +274,7 @@ public class MailingList implements MessageHandler { public boolean membersMayBeListedBy(User user) { if (user.hashPermission(PERMISSION_ADMIN)) return true; try { - if (ListMember.load(this,user).hasState(ListMember.STATE_OWNER)) return true; + if (ListMember.load(this,user).isOwner()) return true; } catch (SQLException e) { LOG.debug("Error loading list member for ({}, {})",user.email(),email()); } @@ -287,8 +302,18 @@ public class MailingList implements MessageHandler { @Override public void onMessageReceived(Message message) throws MessagingException { LOG.debug("Message received: {}",message.getFrom()); - storeMessage(message); - forward(message); + String subject = message.getSubject(); + if (subject.toLowerCase().contains("undelivered")){ + try { + var receivers = members().stream().filter(ListMember::isOwner).map(ListMember::user).map(User::email).toList(); + smtp.forward(email(), receivers, message, message.getSubject(), false,false,null); + } catch (SQLException e){ + LOG.error("Was not able to load members of {}; Non-Delivery notification dropped!",this.email(),e); + } + } else { + storeMessage(message); + forward(message); + } } /** @@ -311,6 +336,10 @@ public class MailingList implements MessageHandler { return list; } + public void replyToList(boolean on) throws SQLException { + setFlag(STATE_REPLY_TO_LIST,on); + } + /** * creates a map of the current ML containing all fields but passwords. @@ -441,6 +470,11 @@ public class MailingList implements MessageHandler { } } + + private String stamp() { + return "["+name+"]"; + } + private void storeMessage(Message message){ // TODO: implement } diff --git a/src/main/java/de/srsoftware/widerhall/mail/SmtpClient.java b/src/main/java/de/srsoftware/widerhall/mail/SmtpClient.java index 3531054..ebc5cdc 100644 --- a/src/main/java/de/srsoftware/widerhall/mail/SmtpClient.java +++ b/src/main/java/de/srsoftware/widerhall/mail/SmtpClient.java @@ -30,21 +30,42 @@ public class SmtpClient { this.port = port; } - public void bccForward(String from, Message message, List emails) throws MessagingException { + + + public void forward(String newSender, List receivers, Message message, String subject, boolean forwardAsAttachment, boolean bcc, String replyTo) throws MessagingException { if (session == null) login(); MimeMessage forward = new MimeMessage(session); - forward.setFrom(from); - forward.setRecipients(Message.RecipientType.BCC,InternetAddress.parse(String.join(", ",emails))); - forward.setSubject(message.getSubject()); + var oldSender = message.getFrom()[0].toString(); + if (newSender != null){ + forward.setFrom(newSender); + forward.setSubject(subject+" (from "+oldSender+")"); + } else { + forward.setFrom(oldSender); + forward.setSubject(subject); + } + if (replyTo != null) forward.setReplyTo(InternetAddress.parse(replyTo)); + var recipientType = bcc ? Message.RecipientType.BCC : Message.RecipientType.TO; + forward.setRecipients(recipientType,InternetAddress.parse(String.join(", ",receivers))); MimeMultipart multipart = new MimeMultipart(); - MimeBodyPart messageBodyPart = new MimeBodyPart(); - - messageBodyPart.setDataHandler(message.getDataHandler()); - multipart.addBodyPart(messageBodyPart); - + if (forwardAsAttachment){ + MimeBodyPart bodyPart = new MimeBodyPart(); + bodyPart.setText("Find the forwarded message in the attachment(s)!\n"); + multipart.addBodyPart(bodyPart); + + // create another body part to contain the message to be forwarded + bodyPart = new MimeBodyPart(); + // forwardedMsg is the MimeMessage object you want to forward as an attachment + bodyPart.setContent(message, "message/rfc822"); + bodyPart.setDisposition(Part.ATTACHMENT); + multipart.addBodyPart(bodyPart); + } else { + MimeBodyPart bodyPart = new MimeBodyPart(); + + bodyPart.setDataHandler(message.getDataHandler()); + multipart.addBodyPart(bodyPart); + } forward.setContent(multipart); - send(forward); } diff --git a/src/main/java/de/srsoftware/widerhall/web/Rest.java b/src/main/java/de/srsoftware/widerhall/web/Rest.java index 1785ef2..c85cda0 100644 --- a/src/main/java/de/srsoftware/widerhall/web/Rest.java +++ b/src/main/java/de/srsoftware/widerhall/web/Rest.java @@ -220,6 +220,8 @@ public class Rest extends HttpServlet { var map = new HashMap<>(); if (list.hasState(MailingList.STATE_FORWARD_FROM)) map.put("forward_from",true); if (list.hasState(MailingList.STATE_FORWARD_ATTACHED)) map.put("forward_attached",true); + if (list.hasState(MailingList.STATE_HIDE_RECEIVERS)) map.put("hide_receivers",true); + if (list.hasState(MailingList.STATE_REPLY_TO_LIST)) map.put("reply_to_list",true); return map; } diff --git a/src/main/java/de/srsoftware/widerhall/web/Web.java b/src/main/java/de/srsoftware/widerhall/web/Web.java index 67dbf76..fe8d5d6 100644 --- a/src/main/java/de/srsoftware/widerhall/web/Web.java +++ b/src/main/java/de/srsoftware/widerhall/web/Web.java @@ -291,6 +291,8 @@ public class Web extends TemplateServlet { try { list.forwardFrom(Util.getCheckbox(req, "forward_from")); list.forwardAttached(Util.getCheckbox(req, "forward_attached")); + list.hideReceivers(Util.getCheckbox(req, "hide_receivers")); + list.replyToList(Util.getCheckbox(req, "reply_to_list")); data.put(NOTES,t("Sucessfully updated MailingList!")); } catch (SQLException e){ LOG.warn("Failed to update MailingList:",e); diff --git a/static/templates/inspect.st b/static/templates/inspect.st index 8089e37..a286c05 100644 --- a/static/templates/inspect.st +++ b/static/templates/inspect.st @@ -19,10 +19,18 @@ Forward using original sender + + diff --git a/static/templates/js.st b/static/templates/js.st index 5300ef8..2cb8226 100644 --- a/static/templates/js.st +++ b/static/templates/js.st @@ -59,6 +59,8 @@ function showListDetail(data){ console.log(data); if (data.forward_from) $('input[name="forward_from"]').prop('checked',true); if (data.forward_attached) $('input[name="forward_attached"]').prop('checked',true); + if (data.hide_receivers) $('input[name="hide_receivers"]').prop('checked',true); + if (data.reply_to_list) $('input[name="reply_to_list"]').prop('checked',true); } function showListOfEditableLists(data){ From 5e01a64a6433c8c0481638240439d18592b2a2d2 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Wed, 20 Apr 2022 13:34:55 +0200 Subject: [PATCH 3/6] =?UTF-8?q?implemented=20rewriting=20of=20"=E2=80=A6?= =?UTF-8?q?=20(from=20)"=20in=20subject,=20prepared=20open=20flag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- .../widerhall/data/MailingList.java | 33 +++++++++++-------- .../srsoftware/widerhall/mail/SmtpClient.java | 7 ++++ .../de/srsoftware/widerhall/web/Rest.java | 1 + .../java/de/srsoftware/widerhall/web/Web.java | 9 ++--- static/templates/inspect.st | 4 +++ static/templates/js.st | 1 + 7 files changed, 39 insertions(+), 18 deletions(-) diff --git a/pom.xml b/pom.xml index 180b480..efb6329 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.example Widerhall - 0.1.1 + 0.1.2 diff --git a/src/main/java/de/srsoftware/widerhall/data/MailingList.java b/src/main/java/de/srsoftware/widerhall/data/MailingList.java index ed71b03..aa2cd3a 100644 --- a/src/main/java/de/srsoftware/widerhall/data/MailingList.java +++ b/src/main/java/de/srsoftware/widerhall/data/MailingList.java @@ -42,6 +42,7 @@ public class MailingList implements MessageHandler { public static final int STATE_FORWARD_ATTACHED = 8; public static final int STATE_HIDE_RECEIVERS = 16; public static final int STATE_REPLY_TO_LIST = 32; + public static final int STATE_OPEN = 64; private static final int VISIBLE = 1; private static final int HIDDEN = 0; private static final int DEFAULT_STATE = STATE_PENDING|STATE_HIDE_RECEIVERS; @@ -134,7 +135,7 @@ public class MailingList implements MessageHandler { } - public void enable(boolean enable) throws SQLException { + public MailingList enable(boolean enable) throws SQLException { setFlag(STATE_ENABLED,enable); if (enable) { @@ -142,6 +143,7 @@ public class MailingList implements MessageHandler { } else { imap.stop(); } + return this; } private void forward(Message message) throws MessagingException { @@ -153,7 +155,7 @@ public class MailingList implements MessageHandler { .map(User::email) .toList(); var subject = message.getSubject(); - // TODO: remove '(from …)' from subject + if (!subject.contains(stamp())) subject = stamp()+" "+subject; var replyTo = (newSender == null && hasState(STATE_REPLY_TO_LIST)) ? email() : null; smtp.forward(newSender,receivers,message,subject,hasState(STATE_FORWARD_ATTACHED),hasState(STATE_HIDE_RECEIVERS),replyTo); @@ -162,12 +164,12 @@ public class MailingList implements MessageHandler { } } - public void forwardAttached(boolean forward) throws SQLException { - setFlag(STATE_FORWARD_ATTACHED,forward); + public MailingList forwardAttached(boolean forward) throws SQLException { + return setFlag(STATE_FORWARD_ATTACHED,forward); } - public void forwardFrom(boolean forward) throws SQLException { - setFlag(STATE_FORWARD_FROM,forward); + public MailingList forwardFrom(boolean forward) throws SQLException { + return setFlag(STATE_FORWARD_FROM,forward); } @@ -201,12 +203,12 @@ public class MailingList implements MessageHandler { return (state & test) > 0; } - public void hide(boolean hide) throws SQLException { - setFlag(STATE_PUBLIC,!hide); + public MailingList hide(boolean hide) throws SQLException { + return setFlag(STATE_PUBLIC,!hide); } - public void hideReceivers(boolean hide) throws SQLException { - setFlag(STATE_HIDE_RECEIVERS,hide); + public MailingList hideReceivers(boolean hide) throws SQLException { + return setFlag(STATE_HIDE_RECEIVERS,hide); } /** @@ -316,6 +318,10 @@ public class MailingList implements MessageHandler { } } + public MailingList open(boolean open) throws SQLException { + return setFlag(STATE_OPEN,open); + } + /** * provide the set of mailing lists that are publicy open to subscriptions * @return @@ -336,8 +342,8 @@ public class MailingList implements MessageHandler { return list; } - public void replyToList(boolean on) throws SQLException { - setFlag(STATE_REPLY_TO_LIST,on); + public MailingList replyToList(boolean on) throws SQLException { + return setFlag(STATE_REPLY_TO_LIST,on); } @@ -399,9 +405,10 @@ public class MailingList implements MessageHandler { smtp.login().send(email(),name(),user.email(),subject,text); } - private void setFlag(int flag, boolean on) throws SQLException { + private MailingList setFlag(int flag, boolean on) throws SQLException { state = on ? state | flag : state ^ (state & flag); Database.open().update(TABLE_NAME).set(STATE,state).where(EMAIL, email()).compile().run(); + return this; } public Map stateMap(){ diff --git a/src/main/java/de/srsoftware/widerhall/mail/SmtpClient.java b/src/main/java/de/srsoftware/widerhall/mail/SmtpClient.java index ebc5cdc..2c0f875 100644 --- a/src/main/java/de/srsoftware/widerhall/mail/SmtpClient.java +++ b/src/main/java/de/srsoftware/widerhall/mail/SmtpClient.java @@ -37,6 +37,13 @@ public class SmtpClient { MimeMessage forward = new MimeMessage(session); var oldSender = message.getFrom()[0].toString(); if (newSender != null){ + var pos = subject.indexOf(" (from "); + while (pos > 0){ + var end = subject.indexOf(')',pos); + if (end < pos) break; + subject = (subject.substring(0,pos)+subject.substring(end+1)).trim(); + pos = subject.indexOf(" (from "); + } forward.setFrom(newSender); forward.setSubject(subject+" (from "+oldSender+")"); } else { diff --git a/src/main/java/de/srsoftware/widerhall/web/Rest.java b/src/main/java/de/srsoftware/widerhall/web/Rest.java index c85cda0..b90367a 100644 --- a/src/main/java/de/srsoftware/widerhall/web/Rest.java +++ b/src/main/java/de/srsoftware/widerhall/web/Rest.java @@ -222,6 +222,7 @@ public class Rest extends HttpServlet { if (list.hasState(MailingList.STATE_FORWARD_ATTACHED)) map.put("forward_attached",true); if (list.hasState(MailingList.STATE_HIDE_RECEIVERS)) map.put("hide_receivers",true); if (list.hasState(MailingList.STATE_REPLY_TO_LIST)) map.put("reply_to_list",true); + if (list.hasState(MailingList.STATE_OPEN)) map.put("open",true); return map; } diff --git a/src/main/java/de/srsoftware/widerhall/web/Web.java b/src/main/java/de/srsoftware/widerhall/web/Web.java index fe8d5d6..516b548 100644 --- a/src/main/java/de/srsoftware/widerhall/web/Web.java +++ b/src/main/java/de/srsoftware/widerhall/web/Web.java @@ -289,10 +289,11 @@ public class Web extends TemplateServlet { if (!error){ try { - list.forwardFrom(Util.getCheckbox(req, "forward_from")); - list.forwardAttached(Util.getCheckbox(req, "forward_attached")); - list.hideReceivers(Util.getCheckbox(req, "hide_receivers")); - list.replyToList(Util.getCheckbox(req, "reply_to_list")); + list.forwardFrom(Util.getCheckbox(req, "forward_from")) + .forwardAttached(Util.getCheckbox(req, "forward_attached")) + .hideReceivers(Util.getCheckbox(req, "hide_receivers")) + .replyToList(Util.getCheckbox(req, "reply_to_list")) + .open(Util.getCheckbox(req,"open")); data.put(NOTES,t("Sucessfully updated MailingList!")); } catch (SQLException e){ LOG.warn("Failed to update MailingList:",e); diff --git a/static/templates/inspect.st b/static/templates/inspect.st index a286c05..071fc44 100644 --- a/static/templates/inspect.st +++ b/static/templates/inspect.st @@ -23,6 +23,10 @@ Set list adddress in "ReplyTo" header + + diff --git a/static/templates/js.st b/static/templates/js.st index 5910ec2..cc06e1d 100644 --- a/static/templates/js.st +++ b/static/templates/js.st @@ -27,6 +27,11 @@ function hideList(listEmail){ $.post('/api/list/hide',{list:listEmail},showListResult,'json'); } +function loadArchive(domain,prefix){ + let listEmail = prefix+'@'+domain; + $.get('/api/list/archive?list='+listEmail,showListArchive,'json'); +} + function loadListDetail(listEmail){ $.post('/api/list/detail',{list:listEmail},showListDetail,'json'); } @@ -55,13 +60,26 @@ function showList(listEmail){ $.post('/api/list/show',{list:listEmail},showListResult,'json'); } +function showListArchive(data){ + for (let time in data.archive){ + let post = data.archive[time]; + let row = $(''); + var url = 'post?id='+post.id; + $('').html(''+new Date(post.date)+'').appendTo(row); + $('').html(''+post.from_name+'').appendTo(row); + $('').html(''+post.subject+'').appendTo(row); + row.appendTo($('#archive')); + console.log(post); + } +} + function showListDetail(data){ - console.log(data); if (data.forward_from) $('input[name="forward_from"]').prop('checked',true); if (data.forward_attached) $('input[name="forward_attached"]').prop('checked',true); if (data.hide_receivers) $('input[name="hide_receivers"]').prop('checked',true); if (data.reply_to_list) $('input[name="reply_to_list"]').prop('checked',true); if (data.open) $('input[name="open"]').prop('checked',true); + if (data.archive) $('input[name="archive"]').prop('checked',true); } function showListOfEditableLists(data){ @@ -112,7 +130,7 @@ function showListList(data){ for (let i in data.lists){ let list = data.lists[i]; let row = $(''); - $('').text(list.name).appendTo(row); + $('').html(''+list.name+'').appendTo(row); $('',{class:'right'}).text(list.email.prefix).appendTo(row); $('',{class:'right'}).text('@').appendTo(row); $('').text(list.email.domain).appendTo(row); diff --git a/static/templates/post.st b/static/templates/post.st new file mode 100644 index 0000000..7da6a49 --- /dev/null +++ b/static/templates/post.st @@ -0,0 +1,34 @@ + + + + + + + + + + «navigation()» + «userinfo()» + «messages()» +

Widerhall Archive:

+ + + + + + + + + + + + + + + + + +
Date«data.date»
From«data.from_name»
Subject«data.subject»
Content
«data.text»
+ «footer()» + + \ No newline at end of file