diff --git a/spring-integration-java-dsl/build.gradle b/spring-integration-java-dsl/build.gradle index 5f7fcdb..8af910a 100644 --- a/spring-integration-java-dsl/build.gradle +++ b/spring-integration-java-dsl/build.gradle @@ -94,6 +94,8 @@ dependencies { } testRuntime "com.sun.mail:mailapi:$mailVersion" testRuntime "com.sun.mail:smtp:$mailVersion" + testRuntime "com.sun.mail:pop3:$mailVersion" + testRuntime "com.sun.mail:imap:$mailVersion" jacoco "org.jacoco:org.jacoco.agent:$jacocoVersion:runtime" } diff --git a/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/ImapMailInboundChannelAdapterSpec.java b/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/ImapMailInboundChannelAdapterSpec.java new file mode 100644 index 0000000..e53358f --- /dev/null +++ b/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/ImapMailInboundChannelAdapterSpec.java @@ -0,0 +1,46 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.integration.dsl.mail; + +import org.springframework.integration.mail.ImapMailReceiver; +import org.springframework.integration.mail.SearchTermStrategy; + +/** + * @author Gary Russell + * + */ +public class ImapMailInboundChannelAdapterSpec + extends MailInboundChannelAdapterSpec { + + ImapMailInboundChannelAdapterSpec() { + this.receiver = new ImapMailReceiver(); + } + + ImapMailInboundChannelAdapterSpec(String url) { + this.receiver = new ImapMailReceiver(url); + } + + public ImapMailInboundChannelAdapterSpec searchTermStrategy(SearchTermStrategy searchTermStrategy) { + this.receiver.setSearchTermStrategy(searchTermStrategy); + return this; + } + + public ImapMailInboundChannelAdapterSpec shouldMarkMessagesAsRead(boolean shouldMarkMessagesAsRead) { + this.receiver.setShouldMarkMessagesAsRead(shouldMarkMessagesAsRead); + return this; + } + +} diff --git a/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/Mail.java b/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/Mail.java index b82c70e..238e385 100644 --- a/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/Mail.java +++ b/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/Mail.java @@ -25,4 +25,25 @@ public class Mail { return new MailSendingMessageHandlerSpec(host); } + public static Pop3MailInboundChannelAdapterSpec pop3InboundAdapter() { + return new Pop3MailInboundChannelAdapterSpec(); + } + + public static Pop3MailInboundChannelAdapterSpec pop3InboundAdapter(String url) { + return new Pop3MailInboundChannelAdapterSpec(url); + } + + public static Pop3MailInboundChannelAdapterSpec pop3InboundAdapter(String host, int port, String username, + String password) { + return new Pop3MailInboundChannelAdapterSpec(host, port, username, password); + } + + public static ImapMailInboundChannelAdapterSpec imapInboundAdapter() { + return new ImapMailInboundChannelAdapterSpec(); + } + + public static ImapMailInboundChannelAdapterSpec imapInboundAdapter(String url) { + return new ImapMailInboundChannelAdapterSpec(url); + } + } diff --git a/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/MailInboundChannelAdapterSpec.java b/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/MailInboundChannelAdapterSpec.java new file mode 100644 index 0000000..6099cda --- /dev/null +++ b/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/MailInboundChannelAdapterSpec.java @@ -0,0 +1,80 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.integration.dsl.mail; + +import java.util.Properties; + +import javax.mail.Authenticator; +import javax.mail.Session; + +import org.springframework.integration.dsl.core.MessageSourceSpec; +import org.springframework.integration.dsl.support.PropertiesBuilder; +import org.springframework.integration.dsl.support.PropertiesConfigurer; +import org.springframework.integration.mail.AbstractMailReceiver; +import org.springframework.integration.mail.MailReceivingMessageSource; + +/** + * @author Gary Russell + * + */ +public abstract class MailInboundChannelAdapterSpec, + R extends AbstractMailReceiver> + extends MessageSourceSpec { + + protected volatile R receiver; + + public S selectorExpression(String selectorExpression) { + this.receiver.setSelectorExpression(PARSER.parseExpression(selectorExpression)); + return _this(); + } + + public S session(Session session) { + this.receiver.setSession(session); + return _this(); + } + + public S javaMailProperties(Properties javaMailProperties) { + this.receiver.setJavaMailProperties(javaMailProperties); + return _this(); + } + + public S javaMailProperties(PropertiesConfigurer configurer) { + PropertiesBuilder properties = new PropertiesBuilder(); + configurer.configure(properties); + return javaMailProperties(properties.get()); + } + + public S javaMailAuthenticator(Authenticator javaMailAuthenticator) { + this.receiver.setJavaMailAuthenticator(javaMailAuthenticator); + return _this(); + } + + public S maxFetchSize(int maxFetchSize) { + this.receiver.setMaxFetchSize(maxFetchSize); + return _this(); + } + + public S shouldDeleteMessages(boolean shouldDeleteMessages) { + this.receiver.setShouldDeleteMessages(shouldDeleteMessages); + return _this(); + } + + @Override + public MailReceivingMessageSource doGet() { + return new MailReceivingMessageSource(this.receiver); + } + +} diff --git a/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/MailSendingMessageHandlerSpec.java b/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/MailSendingMessageHandlerSpec.java index 2f8dfb9..1cad97a 100644 --- a/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/MailSendingMessageHandlerSpec.java +++ b/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/MailSendingMessageHandlerSpec.java @@ -29,8 +29,8 @@ import org.springframework.mail.javamail.JavaMailSenderImpl; * @author Gary Russell * @author Artem Bilan */ -public class MailSendingMessageHandlerSpec extends - MessageHandlerSpec { +public class MailSendingMessageHandlerSpec + extends MessageHandlerSpec { private final JavaMailSenderImpl sender = new JavaMailSenderImpl(); diff --git a/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/Pop3MailInboundChannelAdapterSpec.java b/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/Pop3MailInboundChannelAdapterSpec.java new file mode 100644 index 0000000..62e5fbb --- /dev/null +++ b/spring-integration-java-dsl/src/main/java/org/springframework/integration/dsl/mail/Pop3MailInboundChannelAdapterSpec.java @@ -0,0 +1,43 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.integration.dsl.mail; + +import org.springframework.integration.mail.Pop3MailReceiver; + +/** + * @author Gary Russell + * + */ +public class Pop3MailInboundChannelAdapterSpec + extends MailInboundChannelAdapterSpec { + + Pop3MailInboundChannelAdapterSpec() { + this.receiver = new Pop3MailReceiver(); + } + + Pop3MailInboundChannelAdapterSpec(String url) { + this.receiver = new Pop3MailReceiver(url); + } + + Pop3MailInboundChannelAdapterSpec(String host, String username, String password) { + this.receiver = new Pop3MailReceiver(host, username, password); + } + + Pop3MailInboundChannelAdapterSpec(String host, int port, String username, String password) { + this.receiver = new Pop3MailReceiver(host, port, username, password); + } + +} diff --git a/spring-integration-java-dsl/src/test/java/org/springframework/integration/dsl/test/mail/MailTests.java b/spring-integration-java-dsl/src/test/java/org/springframework/integration/dsl/test/mail/MailTests.java index 3d994df..2da2a56 100644 --- a/spring-integration-java-dsl/src/test/java/org/springframework/integration/dsl/test/mail/MailTests.java +++ b/spring-integration-java-dsl/src/test/java/org/springframework/integration/dsl/test/mail/MailTests.java @@ -18,11 +18,15 @@ package org.springframework.integration.dsl.test.mail; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.endsWith; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import java.util.Properties; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMessage.RecipientType; + import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -35,13 +39,19 @@ import org.springframework.context.annotation.Configuration; import org.springframework.integration.config.EnableIntegration; import org.springframework.integration.dsl.IntegrationFlow; import org.springframework.integration.dsl.IntegrationFlows; +import org.springframework.integration.dsl.channel.MessageChannels; import org.springframework.integration.dsl.mail.Mail; +import org.springframework.integration.dsl.support.Pollers; +import org.springframework.integration.dsl.test.mail.PoorMansMailServer.ImapServer; +import org.springframework.integration.dsl.test.mail.PoorMansMailServer.Pop3Server; import org.springframework.integration.dsl.test.mail.PoorMansMailServer.SmtpServer; import org.springframework.integration.mail.MailHeaders; import org.springframework.integration.support.MessageBuilder; import org.springframework.integration.test.util.TestUtils; +import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHandler; +import org.springframework.messaging.PollableChannel; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -56,15 +66,23 @@ import org.springframework.util.SocketUtils; @DirtiesContext public class MailTests { - private final static int port = SocketUtils.findAvailableTcpPort(); + private final static int smtpPort = SocketUtils.findAvailableTcpPort(); - private static SmtpServer server = PoorMansMailServer.smtp(port); + private static SmtpServer smtpServer = PoorMansMailServer.smtp(smtpPort); + + private final static int pop3Port = SocketUtils.findAvailableTcpPort(smtpPort + 1); + + private static Pop3Server pop3Server = PoorMansMailServer.pop3(pop3Port); + + private final static int imapPort = SocketUtils.findAvailableTcpPort(pop3Port + 1); + + private static ImapServer imapServer = PoorMansMailServer.imap(imapPort); @BeforeClass public static void setup() throws InterruptedException { int n = 0; - while (n++ < 100 && !server.isListening()) { + while (n++ < 100 && (!smtpServer.isListening() || !pop3Server.isListening() || !imapServer.isListening())) { Thread.sleep(100); } assertTrue(n < 100); @@ -72,17 +90,24 @@ public class MailTests { @AfterClass public static void tearDown() { - server.stop(); + smtpServer.stop(); + pop3Server.stop(); + imapServer.stop(); } @Autowired - @Qualifier("sendMailChannel") private MessageChannel sendMailChannel; @Autowired @Qualifier("sendMailEndpoint.handler") private MessageHandler sendMailHandler; + @Autowired + private PollableChannel pop3Channel; + + @Autowired + private PollableChannel imapChannel; + @Test public void testOutbound() throws Exception { assertEquals("localhost", TestUtils.getPropertyValue(this.sendMailHandler, "mailSender.host")); @@ -98,12 +123,12 @@ public class MailTests { .build()); int n = 0; - while (n++ < 100 && server.getMessages().size() == 0) { + while (n++ < 100 && smtpServer.getMessages().size() == 0) { Thread.sleep(100); } - assertTrue(server.getMessages().size() > 0); - String message = server.getMessages().get(0); + assertTrue(smtpServer.getMessages().size() > 0); + String message = smtpServer.getMessages().get(0); assertThat(message, endsWith("foo\n")); assertThat(message, containsString("foo@bar")); assertThat(message, containsString("bar@baz")); @@ -112,6 +137,28 @@ public class MailTests { } + @Test + public void testPop3() throws Exception { + Message message = this.pop3Channel.receive(10000); + assertNotNull(message); + MimeMessage mm = (MimeMessage) message.getPayload(); + assertEquals("foo@bar", mm.getRecipients(RecipientType.TO)[0].toString()); + assertEquals("bar@baz", mm.getFrom()[0].toString()); + assertEquals("Test Email", mm.getSubject()); + assertEquals("foo\r\n", mm.getContent()); + } + + @Test + public void testImap() throws Exception { + Message message = this.imapChannel.receive(10000); + assertNotNull(message); + MimeMessage mm = (MimeMessage) message.getPayload(); + assertEquals("foo@bar", mm.getRecipients(RecipientType.TO)[0].toString()); + assertEquals("bar@baz", mm.getFrom()[0].toString()); + assertEquals("Test Email", mm.getSubject()); + assertEquals("foo\r\n", mm.getContent()); + } + @Configuration @EnableIntegration public static class ContextConfiguration { @@ -120,7 +167,7 @@ public class MailTests { public IntegrationFlow sendMailFlow() { return IntegrationFlows.from("sendMailChannel") .handle(Mail.outboundAdapter("localhost") - .port(port) + .port(smtpPort) .credentials("user", "pw") .protocol("smtp") .javaMailProperties(p -> p.put("mail.debug", "true")), @@ -128,6 +175,28 @@ public class MailTests { .get(); } + @Bean + public IntegrationFlow pop3MailFlow() { + return IntegrationFlows + .from(Mail.pop3InboundAdapter("localhost", pop3Port, "user", "pw") + .javaMailProperties(p -> p.put("mail.debug", "true")), + e -> e.autoStartup(true) + .poller(Pollers.fixedDelay(1000))) + .channel(MessageChannels.queue("pop3Channel")) + .get(); + } + + @Bean + public IntegrationFlow imapMailFlow() { + return IntegrationFlows + .from(Mail.imapInboundAdapter("imap://user:pw@localhost:" + imapPort + "/INBOX") + .javaMailProperties(p -> p.put("mail.debug", "true")), + e -> e.autoStartup(true) + .poller(Pollers.fixedDelay(1000))) + .channel(MessageChannels.queue("imapChannel")) + .get(); + } + } } diff --git a/spring-integration-java-dsl/src/test/java/org/springframework/integration/dsl/test/mail/PoorMansMailServer.java b/spring-integration-java-dsl/src/test/java/org/springframework/integration/dsl/test/mail/PoorMansMailServer.java index 776b2bb..e7517a2 100644 --- a/spring-integration-java-dsl/src/test/java/org/springframework/integration/dsl/test/mail/PoorMansMailServer.java +++ b/spring-integration-java-dsl/src/test/java/org/springframework/integration/dsl/test/mail/PoorMansMailServer.java @@ -46,69 +46,44 @@ public class PoorMansMailServer { } } - public static class SmtpServer implements Runnable { + public static Pop3Server pop3(int port) { + try { + return new Pop3Server(port); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } - private final ServerSocket socket; + public static ImapServer imap(int port) { + try { + return new ImapServer(port); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } - private final ExecutorService exec = Executors.newCachedThreadPool(); - - private final List messages = new ArrayList(); - - private volatile boolean listening; + public static class SmtpServer extends MailServer { public SmtpServer(int port) throws IOException { - this.socket = ServerSocketFactory.getDefault().createServerSocket(port); - this.listening = true; - exec.execute(this); - } - - public boolean isListening() { - return listening; - } - - public List getMessages() { - return messages; + super(port); } @Override - public void run() { - try { - while (!socket.isClosed()) { - Socket socket = this.socket.accept(); - exec.execute(new SmtpHandler(socket)); - } - } - catch (IOException e) { - this.listening = false; - } + protected MailHandler mailHandler(Socket socket) { + return new SmtpHandler(socket); } - public void stop() { - try { - this.socket.close(); - } - catch (IOException e) { - e.printStackTrace(); - } - this.exec.shutdownNow(); - } - - public class SmtpHandler implements Runnable { - - private final Socket socket; - - private BufferedWriter writer; + public class SmtpHandler extends MailHandler { public SmtpHandler(Socket socket) { - this.socket = socket; + super(socket); } @Override - public void run() { + void doRun() { try { - StringBuilder sb = new StringBuilder(); - BufferedReader reader = new BufferedReader(new InputStreamReader(this.socket.getInputStream())); - this.writer = new BufferedWriter(new OutputStreamWriter(this.socket.getOutputStream())); write("220 foo SMTP"); while (!socket.isClosed()) { String line = reader.readLine(); @@ -160,12 +135,253 @@ public class PoorMansMailServer { } } - private void write(String str) throws IOException { + } + + } + + public static class Pop3Server extends MailServer { + + public Pop3Server(int port) throws IOException { + super(port); + } + + @Override + protected MailHandler mailHandler(Socket socket) { + return new Pop3Handler(socket); + } + + public class Pop3Handler extends MailHandler { + + public Pop3Handler(Socket socket) { + super(socket); + } + + @Override + void doRun() { + try { + write("+OK POP3"); + while (!socket.isClosed()) { + String line = reader.readLine(); + switch (line) { + case "CAPA": + write("+OK"); + write("USER"); + write("."); + break; + case "USER user": + write("+OK"); + break; + case "PASS pw": + write("+OK"); + break; + case "STAT": + write("+OK 1 3"); + break; + case "NOOP": + write("+OK"); + break; + case "RETR 1": + write("+OK"); + write(MESSAGE); + write("."); + break; + case "QUIT": + write("+OK"); + socket.close(); + break; + } + } + } + catch (IOException e) { + e.printStackTrace(); + } + } + + } + + } + + public static class ImapServer extends MailServer { + + public ImapServer(int port) throws IOException { + super(port); + } + + @Override + protected MailHandler mailHandler(Socket socket) { + return new Pop3Handler(socket); + } + + public class Pop3Handler extends MailHandler { + + public Pop3Handler(Socket socket) { + super(socket); + } + + @Override + void doRun() { + try { + write("* OK IMAP4rev1 Service Ready"); + while (!socket.isClosed()) { + String line = reader.readLine(); + if (line == null) { + break; + } + String tag = line.substring(0, line.indexOf(" ") + 1); + System.out.println(line); + if (line.endsWith("CAPABILITY")) { + write("* CAPABILITY IMAP4rev1"); + write(tag + "OK CAPABILITY completed"); + } + else if (line.endsWith("LOGIN user pw")) { + write(tag + "OK LOGIN completed"); + } + else if (line.endsWith("LIST \"\" INBOX")) { + write("* LIST \"/\" \"\""); + write(tag + "OK LIST completed"); + } + else if (line.endsWith("EXAMINE \"\"")) { + write("* 1 EXISTS"); + write("* 1 RECENT"); + write("* OK [UNSEEN 1]"); + write(tag + "OK EXAMINE completed"); + } + else if (line.endsWith("SEARCH NOT (FLAGGED) ALL")) { + write("* SEARCH 1 1"); + write(tag + "OK SEARCH completed"); + } + else if (line.contains("FETCH 1,1")) { + write("* 1 FETCH (RFC822.SIZE 6909 INTERNALDATE \"27-May-2013 09:45:41 +0000\" " + + "FLAGS (\\Seen) " + + "ENVELOPE (\"Mon, 27 May 2013 15:14:49 +0530\" " + + "\"Test Email\" ((\"Foo\" NIL \"foo\" \"bar.tv\")) " + + "((\"Foo\" NIL \"foo\" \"bar.tv\")) " + + "((\"Foo\" NIL \"foo\" \"bar.tv\")) " + + "((\"Bar\" NIL \"bar\" \"baz.net\")) NIL NIL " + + "\"<4DA0A7E4.3010506@baz.net>\" " + + "\"\") " + + "BODYSTRUCTURE (\"TEXT\" \"PLAIN\" (\"CHARSET\" \"ISO-8859-1\") NIL NIL \"7BIT\" 1176 43)))"); + write(tag + "OK FETCH completed"); + } + else if (line.contains("STORE 1 +FLAGS (\\Flagged)")) { + write("* 1 FETCH (FLAGS (\\Flagged))"); + write(tag + "OK STORE completed"); + } + else if (line.contains("STORE 1 +FLAGS (\\Seen)")) { + write("* 1 FETCH (FLAGS (\\Flagged \\Seen))"); + write(tag + "OK STORE completed"); + } + else if (line.contains("FETCH 1 FLAGS")) { + write("* 1 FLAGS(\\Seen)"); + write(tag + "OK FETCH completed"); + } + else if (line.contains("FETCH 1 (BODY.PEEK")) { + write("* 1 FETCH (BODY[]<0> {" + (MESSAGE.length() + 2) + "}"); + write(MESSAGE); + write(")"); + write(tag + "OK FETCH completed"); + } + else if (line.contains("CLOSE")) { + write(tag + "OK CLOSE completed"); + } + else if (line.contains("NOOP")) { + write(tag + "OK NOOP completed"); + } + } + } + catch (IOException e) { + e.printStackTrace(); + } + } + + } + + } + + public abstract static class MailServer implements Runnable { + + private final ServerSocket socket; + + private final ExecutorService exec = Executors.newCachedThreadPool(); + + protected final List messages = new ArrayList(); + + private volatile boolean listening; + + public MailServer(int port) throws IOException { + this.socket = ServerSocketFactory.getDefault().createServerSocket(port); + this.listening = true; + exec.execute(this); + } + + public boolean isListening() { + return listening; + } + + public List getMessages() { + return messages; + } + + @Override + public void run() { + try { + while (!socket.isClosed()) { + Socket socket = this.socket.accept(); + exec.execute(mailHandler(socket)); + } + } + catch (IOException e) { + this.listening = false; + } + } + + protected abstract MailHandler mailHandler(Socket socket); + + public void stop() { + try { + this.socket.close(); + } + catch (IOException e) { + e.printStackTrace(); + } + this.exec.shutdownNow(); + } + + public abstract class MailHandler implements Runnable { + + protected static final String MESSAGE = "To: foo@bar\r\nFrom: bar@baz\r\nSubject: Test Email\r\n\r\nfoo"; + + protected final Socket socket; + + private BufferedWriter writer; + + protected StringBuilder sb = new StringBuilder(); + + protected BufferedReader reader; + + public MailHandler(Socket socket) { + this.socket = socket; + } + + @Override + public void run() { + try { + this.reader = new BufferedReader(new InputStreamReader(this.socket.getInputStream())); + this.writer = new BufferedWriter(new OutputStreamWriter(this.socket.getOutputStream())); + } + catch (IOException e) { + e.printStackTrace(); + } + doRun(); + } + + protected void write(String str) throws IOException { this.writer.write(str); this.writer.write("\r\n"); this.writer.flush(); } + abstract void doRun(); }