tomcat-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From r...@locus.apache.org
Subject cvs commit: jakarta-tomcat/proposals/catalina/src/share/org/apache/tomcat/servlets WebdavServlet.java
Date Thu, 03 Aug 2000 17:04:25 GMT
remm        00/08/03 10:04:25

  Modified:    proposals/catalina/src/share/org/apache/tomcat
                        Resources.java
               proposals/catalina/src/share/org/apache/tomcat/resources
                        FileResources.java JarResources.java
                        ResourcesBase.java
               proposals/catalina/src/share/org/apache/tomcat/servlets
                        WebdavServlet.java
  Log:
  - WebDAV COPY support.
  - Cleanups and small fixes in the WebDAV servlet.
  - Added Resources.exists(String path) call, which will return true
    if a resource exists at the given path.
  
  Revision  Changes    Path
  1.6       +15 -4     jakarta-tomcat/proposals/catalina/src/share/org/apache/tomcat/Resources.java
  
  Index: Resources.java
  ===================================================================
  RCS file: /home/cvs/jakarta-tomcat/proposals/catalina/src/share/org/apache/tomcat/Resources.java,v
  retrieving revision 1.5
  retrieving revision 1.6
  diff -u -r1.5 -r1.6
  --- Resources.java	2000/07/30 23:25:16	1.5
  +++ Resources.java	2000/08/03 17:04:21	1.6
  @@ -1,7 +1,7 @@
   /*
  - * $Header: /home/cvs/jakarta-tomcat/proposals/catalina/src/share/org/apache/tomcat/Resources.java,v
1.5 2000/07/30 23:25:16 remm Exp $
  - * $Revision: 1.5 $
  - * $Date: 2000/07/30 23:25:16 $
  + * $Header: /home/cvs/jakarta-tomcat/proposals/catalina/src/share/org/apache/tomcat/Resources.java,v
1.6 2000/08/03 17:04:21 remm Exp $
  + * $Revision: 1.6 $
  + * $Date: 2000/08/03 17:04:21 $
    *
    * ====================================================================
    *
  @@ -83,7 +83,7 @@
    *
    * @author Craig R. McClanahan
    * @author Remy Maucherat
  - * @version $Revision: 1.5 $ $Date: 2000/07/30 23:25:16 $
  + * @version $Revision: 1.6 $ $Date: 2000/08/03 17:04:21 $
    */
   
   public interface Resources {
  @@ -231,6 +231,17 @@
        * @param path The path to the desired resource
        */
       public InputStream getResourceAsStream(String path);
  +
  +
  +    /**
  +     * Returns true if a resource exists at the specified path, 
  +     * where <code>path</code> would be suitable for passing as an argument
to
  +     * <code>getResource()</code> or <code>getResourceAsStream()</code>.
 
  +     * If there is no resource at the specified location, return false.
  +     *
  +     * @param path The path to the desired resource
  +     */
  +    public boolean exists(String path);
   
   
       /**
  
  
  
  1.9       +41 -9     jakarta-tomcat/proposals/catalina/src/share/org/apache/tomcat/resources/FileResources.java
  
  Index: FileResources.java
  ===================================================================
  RCS file: /home/cvs/jakarta-tomcat/proposals/catalina/src/share/org/apache/tomcat/resources/FileResources.java,v
  retrieving revision 1.8
  retrieving revision 1.9
  diff -u -r1.8 -r1.9
  --- FileResources.java	2000/08/01 19:30:17	1.8
  +++ FileResources.java	2000/08/03 17:04:22	1.9
  @@ -1,7 +1,7 @@
   /*
  - * $Header: /home/cvs/jakarta-tomcat/proposals/catalina/src/share/org/apache/tomcat/resources/FileResources.java,v
1.8 2000/08/01 19:30:17 craigmcc Exp $
  - * $Revision: 1.8 $
  - * $Date: 2000/08/01 19:30:17 $
  + * $Header: /home/cvs/jakarta-tomcat/proposals/catalina/src/share/org/apache/tomcat/resources/FileResources.java,v
1.9 2000/08/03 17:04:22 remm Exp $
  + * $Revision: 1.9 $
  + * $Date: 2000/08/03 17:04:22 $
    *
    * ====================================================================
    *
  @@ -93,7 +93,7 @@
    *
    * @author Craig R. McClanahan
    * @author Remy Maucherat
  - * @version $Revision: 1.8 $ $Date: 2000/08/01 19:30:17 $
  + * @version $Revision: 1.9 $ $Date: 2000/08/03 17:04:22 $
    */
   
   public final class FileResources extends ResourcesBase {
  @@ -300,6 +300,30 @@
   
   
       /**
  +     * Returns true if a resource exists at the specified path, 
  +     * where <code>path</code> would be suitable for passing as an argument
to
  +     * <code>getResource()</code> or <code>getResourceAsStream()</code>.
 
  +     * If there is no resource at the specified location, return false.
  +     *
  +     * @param path The path to the desired resource
  +     */
  +    public boolean exists(String path) {
  +        
  +        String normalized = normalize(path);
  +	if (normalized == null)
  +	    return (false);
  +	validate(normalized);
  +        
  +	File file = new File(base, normalized.substring(1));
  +        if (file != null)
  +            return (file.exists());
  +        else
  +            return (false);
  +        
  +    }
  +
  +
  +    /**
        * Return the last modified time for the resource specified by
        * the given virtual path, or -1 if no such resource exists (or
        * the last modified time cannot be determined).
  @@ -500,6 +524,12 @@
               return (false);
           }
           
  +        try {
  +            content.close();
  +        } catch (IOException e) {
  +            return (false);
  +        }
  +        
           return (true);
           
       }
  @@ -539,15 +569,17 @@
       public boolean deleteResource(String path) {
   
           String normalized = normalize(path);
  -	if (normalized == null)
  -	    return (false);
  +	if (normalized == null) {
  +            return (false);
  +        }
   	validate(normalized);
   
   	File file = file(normalized);
  -	if (file != null)
  -	    return (file.delete());
  -	else
  +	if (file != null) {
  +            return (file.delete());
  +	} else {
   	    return (false);
  +        }
   
       }
   
  
  
  
  1.3       +32 -4     jakarta-tomcat/proposals/catalina/src/share/org/apache/tomcat/resources/JarResources.java
  
  Index: JarResources.java
  ===================================================================
  RCS file: /home/cvs/jakarta-tomcat/proposals/catalina/src/share/org/apache/tomcat/resources/JarResources.java,v
  retrieving revision 1.2
  retrieving revision 1.3
  diff -u -r1.2 -r1.3
  --- JarResources.java	2000/07/30 23:20:22	1.2
  +++ JarResources.java	2000/08/03 17:04:22	1.3
  @@ -1,7 +1,7 @@
   /*
  - * $Header: /home/cvs/jakarta-tomcat/proposals/catalina/src/share/org/apache/tomcat/resources/JarResources.java,v
1.2 2000/07/30 23:20:22 remm Exp $
  - * $Revision: 1.2 $
  - * $Date: 2000/07/30 23:20:22 $
  + * $Header: /home/cvs/jakarta-tomcat/proposals/catalina/src/share/org/apache/tomcat/resources/JarResources.java,v
1.3 2000/08/03 17:04:22 remm Exp $
  + * $Revision: 1.3 $
  + * $Date: 2000/08/03 17:04:22 $
    *
    * ====================================================================
    *
  @@ -104,7 +104,7 @@
    * requested the first time (and passes the "cacheable" test).
    *
    * @author Craig R. McClanahan
  - * @version $Revision: 1.2 $ $Date: 2000/07/30 23:20:22 $
  + * @version $Revision: 1.3 $ $Date: 2000/08/03 17:04:22 $
    */
   
   public final class JarResources extends ResourcesBase {
  @@ -305,6 +305,34 @@
   	    }
   	}
   
  +    }
  +
  +
  +    /**
  +     * Returns true if a resource exists at the specified path, 
  +     * where <code>path</code> would be suitable for passing as an argument
to
  +     * <code>getResource()</code> or <code>getResourceAsStream()</code>.
 
  +     * If there is no resource at the specified location, return false.
  +     *
  +     * @param path The path to the desired resource
  +     */
  +    public boolean exists(String path) {
  +        
  +	// Look up and return the last modified time for this resource
  +	String normalized = normalize(path);
  +	if (normalized == null)
  +	    return (false);
  +	validate(normalized);
  +        
  +	ResourceBean resource = null;
  +	synchronized (resourcesCache) {
  +	    resource = (ResourceBean) resourcesCache.get(normalized);
  +	}
  +	if (resource != null)
  +	    return (true);
  +	else
  +	    return (false);
  +        
       }
   
   
  
  
  
  1.4       +15 -4     jakarta-tomcat/proposals/catalina/src/share/org/apache/tomcat/resources/ResourcesBase.java
  
  Index: ResourcesBase.java
  ===================================================================
  RCS file: /home/cvs/jakarta-tomcat/proposals/catalina/src/share/org/apache/tomcat/resources/ResourcesBase.java,v
  retrieving revision 1.3
  retrieving revision 1.4
  diff -u -r1.3 -r1.4
  --- ResourcesBase.java	2000/08/01 02:41:23	1.3
  +++ ResourcesBase.java	2000/08/03 17:04:22	1.4
  @@ -1,7 +1,7 @@
   /*
  - * $Header: /home/cvs/jakarta-tomcat/proposals/catalina/src/share/org/apache/tomcat/resources/ResourcesBase.java,v
1.3 2000/08/01 02:41:23 remm Exp $
  - * $Revision: 1.3 $
  - * $Date: 2000/08/01 02:41:23 $
  + * $Header: /home/cvs/jakarta-tomcat/proposals/catalina/src/share/org/apache/tomcat/resources/ResourcesBase.java,v
1.4 2000/08/03 17:04:22 remm Exp $
  + * $Revision: 1.4 $
  + * $Date: 2000/08/03 17:04:22 $
    *
    * ====================================================================
    *
  @@ -101,7 +101,7 @@
    * (such as a local or remote JAR file).
    *
    * @author Craig R. McClanahan
  - * @version $Revision: 1.3 $ $Date: 2000/08/01 02:41:23 $
  + * @version $Revision: 1.4 $ $Date: 2000/08/03 17:04:22 $
    */
   
   public abstract class ResourcesBase
  @@ -618,6 +618,17 @@
        * @param path The path to the desired resource
        */
       public abstract InputStream getResourceAsStream(String path);
  +
  +
  +    /**
  +     * Returns true if a resource exists at the specified path, 
  +     * where <code>path</code> would be suitable for passing as an argument
to
  +     * <code>getResource()</code> or <code>getResourceAsStream()</code>.
 
  +     * If there is no resource at the specified location, return false.
  +     *
  +     * @param path The path to the desired resource
  +     */
  +    public abstract boolean exists(String path);
   
   
       /**
  
  
  
  1.4       +249 -82   jakarta-tomcat/proposals/catalina/src/share/org/apache/tomcat/servlets/WebdavServlet.java
  
  Index: WebdavServlet.java
  ===================================================================
  RCS file: /home/cvs/jakarta-tomcat/proposals/catalina/src/share/org/apache/tomcat/servlets/WebdavServlet.java,v
  retrieving revision 1.3
  retrieving revision 1.4
  diff -u -r1.3 -r1.4
  --- WebdavServlet.java	2000/08/02 17:18:19	1.3
  +++ WebdavServlet.java	2000/08/03 17:04:24	1.4
  @@ -1,7 +1,7 @@
   /*
  - * $Header: /home/cvs/jakarta-tomcat/proposals/catalina/src/share/org/apache/tomcat/servlets/WebdavServlet.java,v
1.3 2000/08/02 17:18:19 remm Exp $
  - * $Revision: 1.3 $
  - * $Date: 2000/08/02 17:18:19 $
  + * $Header: /home/cvs/jakarta-tomcat/proposals/catalina/src/share/org/apache/tomcat/servlets/WebdavServlet.java,v
1.4 2000/08/03 17:04:24 remm Exp $
  + * $Revision: 1.4 $
  + * $Date: 2000/08/03 17:04:24 $
    *
    * ====================================================================
    *
  @@ -118,7 +118,7 @@
    * are handled by the DefaultServlet.
    *
    * @author Remy Maucherat
  - * @version $Revision: 1.3 $ $Date: 2000/08/02 17:18:19 $
  + * @version $Revision: 1.4 $ $Date: 2000/08/03 17:04:24 $
    */
   
   public class WebdavServlet
  @@ -292,21 +292,14 @@
           
           // Retrieve the Catalina context
           ApplicationContext context = (ApplicationContext) getServletContext();
  -
  -	// Convert the resource path to a URL
  -	URL resourceURL = null;
  -	try {
  -	    resourceURL = context.getResource(servletPath);
  -	} catch (MalformedURLException e) {
  -	    ;
  -	}
  -	if (resourceURL == null) {
  +        
  +        Resources resources = context.getResources();
  +        
  +	if (!resources.exists(servletPath)) {
   	    methodsAllowed = "OPTIONS, MKCOL, PUT, LOCK";
               resp.addHeader("Allow", methodsAllowed);
               return;
   	}
  -
  -        Resources resources = context.getResources();
           
           methodsAllowed = "OPTIONS, GET, HEAD, POST, DELETE, TRACE, " 
               + "PROPFIND, PROPPATCH, COPY, MOVE, LOCK, UNLOCK";
  @@ -418,22 +411,15 @@
           // Retrieve the Catalina context
           ApplicationContext context = (ApplicationContext) getServletContext();
   
  -	// Convert the resource path to a URL
  -	URL resourceURL = null;
  -	try {
  -	    resourceURL = context.getResource(servletPath);
  -	} catch (MalformedURLException e) {
  -	    ;
  -	}
  -	if (resourceURL == null) {
  +        Resources resources = context.getResources();
  +        
  +	if (!resources.exists(servletPath)) {
   	    resp.sendError(HttpServletResponse.SC_NOT_FOUND, servletPath);
   	    return;
   	}
   
           resp.setStatus(WebdavStatus.SC_MULTI_STATUS);
   
  -        Resources resources = context.getResources();
  -        
           // Create multistatus object
           XMLWriter generatedXML = new XMLWriter();
           
  @@ -515,20 +501,15 @@
           // Retrieve the Catalina context
           ApplicationContext context = (ApplicationContext) getServletContext();
   
  -	// Convert the resource path to a URL
  -	URL resourceURL = null;
  -	try {
  -	    resourceURL = context.getResource(servletPath);
  -	} catch (MalformedURLException e) {
  -	    ;
  -	}
  -	if (resourceURL != null) {
  +        Resources resources = context.getResources();
  +        
  +	// Can't create a collection if a resource already exists at the given
  +        // path
  +	if (resources.exists(servletPath)) {
   	    resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED);
   	    return;
   	}
   
  -        Resources resources = context.getResources();
  -        
           boolean result = resources.createCollection(servletPath);
           
           if (!result) {
  @@ -552,51 +533,8 @@
               resp.sendError(WebdavStatus.SC_FORBIDDEN);
               return;
           }
  -        
  -	String servletPath = req.getServletPath();
  -	if (servletPath == null)
  -	    servletPath = "/";
  -        
  -        // Retrieve the Catalina context
  -        ApplicationContext context = (ApplicationContext) getServletContext();
  -
  -	// Convert the resource path to a URL
  -	URL resourceURL = null;
  -	try {
  -	    resourceURL = context.getResource(servletPath);
  -	} catch (MalformedURLException e) {
  -	    ;
  -	}
  -	if (resourceURL == null) {
  -	    resp.sendError(WebdavStatus.SC_NOT_FOUND);
  -	    return;
  -	}
  -
  -        Resources resources = context.getResources();
  -        
  -        boolean collection = resources.isCollection(servletPath);
  -        
  -        if (!collection) {
  -            if (!resources.deleteResource(servletPath)) {
  -                resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);
  -                return;
  -            }
  -        } else {
  -            
  -            Hashtable errorList = new Hashtable();
  -            
  -            deleteCollection(resources, servletPath, errorList);
  -            resources.deleteResource(servletPath);
  -            
  -            if (!errorList.isEmpty()) {
  -                
  -                // TODO : Display a status report if there was an error
  -                
  -            }
  -            
  -        }
           
  -        resp.setStatus(WebdavStatus.SC_NO_CONTENT);
  +        deleteResource(req, resp);
           
       }
   
  @@ -620,7 +558,7 @@
           ApplicationContext context = (ApplicationContext) getServletContext();
           Resources resources = context.getResources();
           
  -        boolean exists = (resources.getResourceAsStream(servletPath) != null);
  +        boolean exists = resources.exists(servletPath);
           
           boolean result = resources.setResource(servletPath, 
                                                  req.getInputStream());
  @@ -648,8 +586,8 @@
               resp.sendError(WebdavStatus.SC_FORBIDDEN);
               return;
           }
  -        
           
  +        copyResource(req, resp);
           
       }
   
  @@ -665,8 +603,14 @@
               return;
           }
           
  +	String servletPath = req.getServletPath();
  +	if (servletPath == null)
  +	    servletPath = "/";
           
  -
  +        if (copyResource(req, resp)) {
  +            deleteResource(servletPath, resp);
  +        }
  +        
       }
   
   
  @@ -696,6 +640,223 @@
   
   
       /**
  +     * Copy a resource.
  +     * 
  +     * @param req Servlet request
  +     * @param resp Servlet response
  +     * @return boolean true if the copy is successful
  +     */
  +    private boolean copyResource(HttpServletRequest req, 
  +                                 HttpServletResponse resp)
  +        throws ServletException, IOException {
  +        
  +        // Parsing destination header
  +        
  +        String destinationPath = req.getHeader("Destination");
  +        System.out.println("Dest path:" + destinationPath);
  +        
  +        if (destinationPath.startsWith("http://")) {
  +            destinationPath = destinationPath.substring("http://".length());
  +        }
  +        
  +        String hostName = req.getServerName();
  +        if ((hostName != null) && (destinationPath.startsWith(hostName))) {
  +            destinationPath = destinationPath.substring(hostName.length());
  +        }
  +        System.out.println("Dest path 2:" + destinationPath);
  +        
  +        if (destinationPath.startsWith(":")) {
  +            int firstSeparator = destinationPath.indexOf("/");
  +            if (firstSeparator < 0) {
  +                destinationPath = "/";
  +            } else {
  +                destinationPath = destinationPath.substring(firstSeparator);
  +            }
  +        }
  +        System.out.println("Dest path 3:" + destinationPath);
  +        
  +        /*
  +          String servletPath = req.getServletPath();
  +          if ((servletPath != null) && 
  +          (destinationPath.startsWith(servletPath))) {
  +          destinationPath = destinationPath
  +          .substring(servletPath.length());
  +          }
  +          System.out.println("Dest path 4:" + destinationPath);
  +        */
  +        
  +	String servletPath = req.getServletPath();
  +	if (servletPath == null)
  +	    servletPath = "/";
  +        
  +        if (destinationPath.equals(servletPath)) {
  +            resp.sendError(WebdavStatus.SC_FORBIDDEN);
  +            return false;
  +        }
  +        
  +        // Parsing overwrite header
  +        
  +        boolean overwrite = true;
  +        String overwriteHeader = req.getHeader("Overwrite");
  +        
  +        if (overwriteHeader != null) {
  +            if (overwriteHeader.equalsIgnoreCase("T")) {
  +                overwrite = true;
  +            } else {
  +                overwrite = false;
  +            }
  +        }
  +        
  +        // Overwriting the destination
  +        
  +        // Retrieve the Catalina context
  +        ApplicationContext context = (ApplicationContext) getServletContext();
  +        
  +        Resources resources = context.getResources();
  +        
  +        if (overwrite) {
  +            
  +            // Delete destination resource, if it exists
  +            if (resources.exists(destinationPath)) {
  +                if (!deleteResource(destinationPath, resp)) {
  +                    return false;
  +                } else {
  +                    resp.setStatus(WebdavStatus.SC_NO_CONTENT);
  +                }
  +            } else {
  +                resp.setStatus(WebdavStatus.SC_CREATED);
  +            }
  +            
  +        } else {
  +            
  +            // If the destination exists, then it's a conflict
  +            if (resources.exists(destinationPath)) {
  +                resp.sendError(WebdavStatus.SC_PRECONDITION_FAILED);
  +                return false;
  +            }
  +            
  +        }
  +        
  +        // Copying source to destination
  +        
  +        Hashtable errorList = new Hashtable();
  +        
  +        boolean result = copyResource(resources, errorList, 
  +                                      servletPath, destinationPath);
  +        
  +        return true;
  +        
  +    }
  +
  +
  +    /**
  +     * Copy a collection.
  +     * 
  +     * @param resources Resources implementation to be used
  +     * @param errorList Hashtable containing the list of errors which occured
  +     * during the copy operation
  +     * @param source Path of the resource to be copied
  +     * @param dest Destination path
  +     */
  +    private boolean copyResource(Resources resources, Hashtable errorList,
  +                                 String source, String dest) {
  +        
  +        if (resources.isCollection(source)) {
  +            
  +            if (!resources.createCollection(dest)) {
  +                // Add the error to the list
  +                return false;
  +            }
  +            String[] members = resources.getCollectionMembers(source);
  +            for (int i=0; i<members.length; i++) {
  +                String childDest = dest + 
  +                    members[i].substring(source.length());
  +                copyResource(resources, errorList, members[i], childDest);
  +            }
  +            
  +        } else {
  +            
  +            InputStream is = resources.getResourceAsStream(source);
  +            if (!resources.setResource(dest, is)) {
  +                // Add the error to the list
  +                return false;
  +            }
  +            
  +        }
  +        
  +        return true;
  +        
  +    }
  +
  +
  +    /**
  +     * Delete a resource.
  +     * 
  +     * @param req Servlet request
  +     * @param resp Servlet response
  +     * @return boolean true if the copy is successful
  +     */
  +    private boolean deleteResource(HttpServletRequest req, 
  +                                   HttpServletResponse resp)
  +        throws ServletException, IOException {
  +        
  +	String servletPath = req.getServletPath();
  +	if (servletPath == null)
  +	    servletPath = "/";
  +        
  +        return deleteResource(servletPath, resp);
  +        
  +    }
  +
  +
  +    /**
  +     * Delete a resource.
  +     * 
  +     * @param path Path of the resource which is to be deleted
  +     * @param resp Servlet response
  +     */
  +    private boolean deleteResource(String path, HttpServletResponse resp)
  +        throws ServletException, IOException {
  +        
  +        // Retrieve the Catalina context
  +        ApplicationContext context = (ApplicationContext) getServletContext();
  +
  +        Resources resources = context.getResources();
  +        
  +	if (!resources.exists(path)) {
  +            resp.sendError(WebdavStatus.SC_NOT_FOUND);
  +	    return false;
  +	}
  +        
  +        boolean collection = resources.isCollection(path);
  +        
  +        if (!collection) {
  +            if (!resources.deleteResource(path)) {
  +                resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);
  +                return false;
  +            }
  +        } else {
  +            
  +            Hashtable errorList = new Hashtable();
  +            
  +            deleteCollection(resources, path, errorList);
  +            resources.deleteResource(path);
  +            
  +            if (!errorList.isEmpty()) {
  +                
  +                // TODO : Display a status report if there was an error
  +                
  +            }
  +            
  +        }
  +        
  +        resp.setStatus(WebdavStatus.SC_NO_CONTENT);
  +        return true;
  +        
  +    }
  +
  +
  +    /**
        * Propfind helper method.
        * 
        * @param resources Resources object associated with this context
  @@ -768,6 +929,9 @@
               String supportedLocks = "<d:lockentry>" 
                   + "<d:lockscope><d:exclusive/></d:lockscope>"
                   + "<d:locktype><d:write/></d:locktype>"
  +                + "</d:lockentry>" + "<d:lockentry>" 
  +                + "<d:lockscope><d:shared/></d:lockscope>"
  +                + "<d:locktype><d:write/></d:locktype>"
                   + "</d:lockentry>";
               generatedXML.writeProperty("d", "DAV", "supportedlock", 
                                          supportedLocks);
  @@ -894,6 +1058,9 @@
                   } else if (property.equals("supportedlock")) {
                       supportedLocks = "<d:lockentry>" 
                           + "<d:lockscope><d:exclusive/></d:lockscope>"
  +                        + "<d:locktype><d:write/></d:locktype>"
  +                        + "</d:lockentry>" + "<d:lockentry>" 
  +                        + "<d:lockscope><d:shared/></d:lockscope>"
                           + "<d:locktype><d:write/></d:locktype>"
                           + "</d:lockentry>";
                       generatedXML.writeProperty("d", "DAV", "supportedlock",
  
  
  

Mime
View raw message