aboutsummaryrefslogblamecommitdiff
path: root/src/main/java/com/juick/www/api/Service.java
blob: 1e3dcdc89d96aff9c5d0f3f8d7c50c6c1afec123 (plain) (tree)
1
2
  
                                 













                                                                           
                          
 
                               
                                     



                                              
                                      
                                         
                                        
                                     
                                                            
                                            
                                               



                                              
                                                   
                                                  
                                            
                                                 


                                                          
                                                             
                                                 
                                                 
                                                                                  
                                                                        
                           




                                         
                   

                      
                                                                            
                                    
           
                                      
                                            
                                            
                                                                
                             
                                          
                                   
                                        
 
                                                                                 
 
           
                             
                                          
                                                                                                        
                                          
                                                             




                                                                           
             





                                                                                   
 



























                                                                                                                   
                                     
                                                                                           
                                                                                                                 
                                                                          
                                                         
                                                                       
                                 

                                                                                  
                                                                                                            


                                                                   
                                                                             
                                                                              









                                                                                             
                         
                     











                                                                                                               
                     




                                                                                                          
                     
                 
                                                                   
                

                                               
 
           
                                          
                                                                                                                   
                                          
                                                             
                                                                           


                                                                        

                                                                
                

                                               
 
                                                 
                                                                                        
                                                                                 
 
                              
                                                                      


                                                       
                                                     
                                                                                          
 
                                                        

                       
 


                                                         
 
/*
 * Copyright (C) 2008-2020, Juick
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.juick.www.api;

import com.juick.model.Message;
import com.juick.model.User;
import com.juick.model.CommandResult;
import com.juick.CommandsManager;
import com.juick.EmailManager;
import com.juick.ServerManager;
import com.juick.util.HttpBadRequestException;
import com.juick.util.HttpForbiddenException;
import com.juick.service.EmailService;
import com.juick.service.MessagesService;
import com.juick.service.StorageService;
import com.juick.service.UserService;
import com.juick.service.component.AccountVerificationEvent;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
import jakarta.mail.Session;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.commons.mail.util.MimeMessageParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import javax.inject.Inject;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.util.*;

@Controller
public class Service {
    private static final Logger logger = LoggerFactory.getLogger("Session");
    @Inject
    private UserService userService;
    @Inject
    private EmailService emailService;
    @Inject
    private MessagesService messagesService;
    @Inject
    private CommandsManager commandsManager;
    @Inject
    private ApplicationEventPublisher applicationEventPublisher;
    @Inject
    private User serviceUser;
    @Inject
    private StorageService storageService;
    @Value("${banned_emails:}")
    private String[] ignoredEmails;
    @Inject
    private ServerManager serverManager;

    private final Session session = Session.getDefaultInstance(new Properties());

    @Hidden
    @PostMapping("/api/mail")
    @ResponseStatus(value = HttpStatus.OK)
    public void processMail(@Parameter(hidden = true) User current, InputStream data) throws Exception {
        if (current.equals(serviceUser)) {
            MimeMessage msg = new MimeMessage(session, data);
            String[] returnPaths = msg.getHeader("Return-Path");
            if (returnPaths != null) {
                logger.info("got msg with return path {}", returnPaths[0]);
                if (returnPaths[0].equals("<>")) {
                    return;
                }
            }
            try {
                var messageAddresses = msg.getFrom();
                if (messageAddresses == null || messageAddresses.length == 0) {
                    logger.info("Missing from/sender headers");
                    return;
                }
                String from = ((InternetAddress) messageAddresses[0]).getAddress();

                User visitor = userService.getUserByEmail(from);
                if (!visitor.isAnonymous()) {
                    MimeMessageParser parser = new MimeMessageParser(msg);
                    parser.parse();
                    final String[] body = { parser.getPlainContent() };
                    if (body[0] == null) {
                        parser.getAttachmentList().stream().filter(a -> a.getContentType().equals("text/plain"))
                                .findFirst().ifPresent(a -> {
                                    try {
                                        body[0] = IOUtils.toString(a.getInputStream(), StandardCharsets.UTF_8);
                                        logger.info("got text: {}", body[0]);
                                    } catch (IOException e) {
                                        logger.info("attachment error", e);
                                    }
                                });
                    }
                    final String[] attachmentFName = new String[1];
                    parser.getAttachmentList().stream().filter(
                            a -> a.getContentType().equals("image/jpeg") || a.getContentType().equals("image/png"))
                            .findFirst().ifPresent(a -> {
                                logger.info("got attachment: {}", a.getContentType());
                                String attachmentType;
                                if (a.getContentType().equals("image/jpeg")) {
                                    attachmentType = "jpg";
                                } else {
                                    attachmentType = "png";
                                }
                                attachmentFName[0] = DigestUtils.md5Hex(UUID.randomUUID().toString()) + "."
                                        + attachmentType;
                                try {
                                    logger.info("got inputstream: {}", a.getInputStream());
                                    FileOutputStream fos = new FileOutputStream(
                                            Paths.get(storageService.getTemporaryDirectory(), attachmentFName[0])
                                                    .toString());
                                    IOUtils.copy(a.getInputStream(), fos);
                                    fos.close();
                                } catch (IOException e) {
                                    logger.info("attachment error", e);
                                }
                            });
                    String[] inReplyToHeaders = msg.getHeader("In-Reply-To");
                    if (inReplyToHeaders != null && inReplyToHeaders.length > 0) {
                        int mid, rid;
                        String inReplyTo = inReplyToHeaders[0].trim();
                        var originalMessage = messagesService.findMessageByProperty("messageId", inReplyTo);
                        if (originalMessage.isPresent()) {
                            mid = originalMessage.get().getLeft();
                            rid = originalMessage.get().getRight();
                        } else {
                            Scanner inReplyToScanner = new Scanner(inReplyTo)
                                    .useDelimiter(EmailManager.MSGID_PATTERN);
                            mid = NumberUtils.toInt(inReplyToScanner.next(), 0);
                            rid = NumberUtils.toInt(inReplyToScanner.next(), 0);
                            inReplyToScanner.close();
                        }
                        if (mid > 0) {
                            logger.info("Message is reply to #{}/{}", mid, rid);
                            body[0] = rid > 0 ? String.format("#%d/%d %s", mid, rid, body[0])
                                    : String.format("#%d %s", mid, body[0]);
                        } else {
                            logger.warn("Unknown In-Reply-To: {}", inReplyTo);
                            return;
                        }
                    }
                    URI attachmentUri = StringUtils.isNotEmpty(attachmentFName[0])
                            ? URI.create(String.format("juick://%s", attachmentFName[0]))
                            : URI.create(StringUtils.EMPTY);
                    CommandResult result = commandsManager.processCommand(visitor, body[0], attachmentUri);
                    if (result.getNewMessage().isPresent()) {
                        String[] messageIds = msg.getHeader("Message-Id");
                        if (messageIds.length == 1) {
                            Message message = result.getNewMessage().get();
                            messagesService.setMessageProperty(message.getMid(), message.getRid(), "messageId",
                                    messageIds[0]);
                        } else {
                            logger.warn("Wrong number of Message-Id headers");
                        }
                    }
                } else {
                    if (!Arrays.asList(ignoredEmails).contains(from)) {
                        String verificationCode = RandomStringUtils.randomAlphanumeric(8).toUpperCase();
                        emailService.addVerificationCode(null, from, verificationCode);
                        applicationEventPublisher
                                .publishEvent(new AccountVerificationEvent(this, from, verificationCode));
                    }
                }
            } catch (AddressException e) {
                logger.info("Got email with invalid from address");
            }
        } else {
            throw new HttpForbiddenException();
        }
    }

    @Hidden
    @PostMapping("/api/mail/unsubscribe")
    @ResponseStatus(value = HttpStatus.OK)
    public void processMailUnsubscribe(@Parameter(hidden = true) User current, InputStream data) throws Exception {
        if (current.equals(serviceUser)) {
            MimeMessage msg = new MimeMessage(session, data);
            String from = msg.getFrom() == null || msg.getFrom().length > 1
                    ? ((InternetAddress) msg.getSender()).getAddress()
                    : ((InternetAddress) msg.getFrom()[0]).getAddress();

            User visitor = userService.getUserByEmail(from);
            if (!visitor.isAnonymous()) {
                if (!emailService.disableEmail(visitor, from)) {
                    throw new HttpBadRequestException();
                }
            }
        } else {
            throw new HttpForbiddenException();
        }
    }

    private void endSession(SseEmitter emitter) {
        serverManager.getSessions().stream().filter(s -> s.getEmitter().equals(emitter))
                .forEach(session -> serverManager.getSessions().remove(session));
    }

    @GetMapping("/api/events")
    public SseEmitter handle(@Parameter(hidden = true) User visitor) {
        logger.info("{} connected", visitor.getName());
        if (!visitor.isAnonymous()) {
            userService.updateLastSeen(visitor);
        }
        SseEmitter emitter = new SseEmitter(600000L);
        serverManager.getSessions().add(new ServerManager.EventSession(visitor, emitter));

        emitter.onCompletion(() -> endSession(emitter));
        emitter.onTimeout(() -> endSession(emitter));

        return emitter;
    }

    @ExceptionHandler(AsyncRequestTimeoutException.class)
    public void eventErrorHandler(Exception ex) {
        logger.debug("SSE timeout", ex);
    }
}